From 6196004b51a6850909c154f5402ff4858eab479a Mon Sep 17 00:00:00 2001 From: grothedev Date: Fri, 29 May 2026 21:49:20 -0400 Subject: mv web stuff to root project dir --- .claude/settings.local.json | 10 + .gitignore | 5 +- AGENTS.md | 46 + ARCHITECTURE.md | 194 ++++ LICENSE | 674 +++++++++++++ PROTOTYPING.md | 146 +++ README.md | 147 +++ WEBSOCKET_FORMAT.md | 117 +++ bun.lock | 150 +++ data1.txt | 50 + data2.txt | 50 + data3.txt | 50 + data4.txt | 30 + dist/assets/SharedSystems-DVK37F7d.js | 326 +++++++ dist/assets/WebGLRenderer-BVO0qznQ.js | 156 +++ dist/assets/WebGPURenderer-CtyVoQqf.js | 41 + dist/assets/browserAll-BytxDfn1.js | 14 + dist/assets/colorToUniform-DmtBy-2V.js | 31 + dist/assets/index-DB6B0XuI.js | 451 +++++++++ dist/assets/index-DCiDMyds.css | 1 + dist/assets/webworkerAll-J8ccMaq4.js | 296 ++++++ dist/demo-data/chirp-ramp.csv | 47 + dist/demo-data/step-bursts.csv | 42 + dist/demo-data/telemetry-sweep.csv | 42 + dist/index.html | 13 + doc/ui.md.uxf | 22 + index.html | 12 + package-lock.json | 1071 +++++++++++++++++++++ package.json | 19 + public/demo-data/chirp-ramp.csv | 47 + public/demo-data/step-bursts.csv | 42 + public/demo-data/telemetry-sweep.csv | 42 + sample_data.txt | 30 + scripts/demo-websocket-server.mjs | 131 +++ src/app/create-app.js | 449 +++++++++ src/bootstrap.js | 18 + src/core/event-bus.js | 32 + src/core/store.js | 291 ++++++ src/core/time-controller.js | 80 ++ src/data-sources.js | 517 ++++++++++ src/data/base-source.js | 21 + src/data/csv-replay-source.js | 60 ++ src/data/parse-replay-csv.js | 108 +++ src/data/source-registry.js | 90 ++ src/data/synthetic-wave-source.js | 87 ++ src/data/websocket-source.js | 224 +++++ src/demos.js | 697 ++++++++++++++ src/example-usage.js | 535 ++++++++++ src/main.js | 1 + src/metrics.js | 142 +++ src/plot-connections.js | 392 ++++++++ src/plot/plot-buffer.js | 22 + src/plot/timeplot-view.js | 442 +++++++++ src/state.js | 420 ++++++++ src/styles.css | 401 ++++++++ src/template-for-standard-site.js | 75 ++ src/test-data-generators.js | 530 ++++++++++ src/timeseries-plot.js | 277 ++++++ src/ui/panel-manager.js | 542 +++++++++++ src/utils-format.js | 22 + src/waterfall.js | 219 +++++ web-timeplot/.gitignore | 1 - web-timeplot/AGENTS.md | 46 - web-timeplot/ARCHITECTURE.md | 194 ---- web-timeplot/PROTOTYPING.md | 146 --- web-timeplot/README.md | 147 --- web-timeplot/WEBSOCKET_FORMAT.md | 117 --- web-timeplot/bun.lock | 150 --- web-timeplot/index.html | 12 - web-timeplot/package-lock.json | 1071 --------------------- web-timeplot/package.json | 19 - web-timeplot/public/demo-data/chirp-ramp.csv | 47 - web-timeplot/public/demo-data/step-bursts.csv | 42 - web-timeplot/public/demo-data/telemetry-sweep.csv | 42 - web-timeplot/scripts/demo-websocket-server.mjs | 131 --- web-timeplot/src/app/create-app.js | 449 --------- web-timeplot/src/bootstrap.js | 18 - web-timeplot/src/core/event-bus.js | 32 - web-timeplot/src/core/store.js | 291 ------ web-timeplot/src/core/time-controller.js | 80 -- web-timeplot/src/data-sources.js | 517 ---------- web-timeplot/src/data/base-source.js | 21 - web-timeplot/src/data/csv-replay-source.js | 60 -- web-timeplot/src/data/parse-replay-csv.js | 108 --- web-timeplot/src/data/source-registry.js | 90 -- web-timeplot/src/data/synthetic-wave-source.js | 87 -- web-timeplot/src/data/websocket-source.js | 224 ----- web-timeplot/src/demos.js | 697 -------------- web-timeplot/src/example-usage.js | 535 ---------- web-timeplot/src/main.js | 1 - web-timeplot/src/metrics.js | 142 --- web-timeplot/src/plot-connections.js | 392 -------- web-timeplot/src/plot/plot-buffer.js | 22 - web-timeplot/src/plot/timeplot-view.js | 442 --------- web-timeplot/src/state.js | 420 -------- web-timeplot/src/styles.css | 401 -------- web-timeplot/src/template-for-standard-site.js | 75 -- web-timeplot/src/test-data-generators.js | 530 ---------- web-timeplot/src/timeseries-plot.js | 277 ------ web-timeplot/src/ui/panel-manager.js | 542 ----------- web-timeplot/src/utils-format.js | 22 - web-timeplot/src/waterfall.js | 219 ----- 102 files changed, 11235 insertions(+), 8863 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 AGENTS.md create mode 100644 ARCHITECTURE.md create mode 100644 LICENSE create mode 100644 PROTOTYPING.md create mode 100644 README.md create mode 100644 WEBSOCKET_FORMAT.md create mode 100644 bun.lock create mode 100644 data1.txt create mode 100644 data2.txt create mode 100644 data3.txt create mode 100644 data4.txt create mode 100644 dist/assets/SharedSystems-DVK37F7d.js create mode 100644 dist/assets/WebGLRenderer-BVO0qznQ.js create mode 100644 dist/assets/WebGPURenderer-CtyVoQqf.js create mode 100644 dist/assets/browserAll-BytxDfn1.js create mode 100644 dist/assets/colorToUniform-DmtBy-2V.js create mode 100644 dist/assets/index-DB6B0XuI.js create mode 100644 dist/assets/index-DCiDMyds.css create mode 100644 dist/assets/webworkerAll-J8ccMaq4.js create mode 100644 dist/demo-data/chirp-ramp.csv create mode 100644 dist/demo-data/step-bursts.csv create mode 100644 dist/demo-data/telemetry-sweep.csv create mode 100644 dist/index.html create mode 100644 doc/ui.md.uxf create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/demo-data/chirp-ramp.csv create mode 100644 public/demo-data/step-bursts.csv create mode 100644 public/demo-data/telemetry-sweep.csv create mode 100644 sample_data.txt create mode 100644 scripts/demo-websocket-server.mjs create mode 100644 src/app/create-app.js create mode 100644 src/bootstrap.js create mode 100644 src/core/event-bus.js create mode 100644 src/core/store.js create mode 100644 src/core/time-controller.js create mode 100644 src/data-sources.js create mode 100644 src/data/base-source.js create mode 100644 src/data/csv-replay-source.js create mode 100644 src/data/parse-replay-csv.js create mode 100644 src/data/source-registry.js create mode 100644 src/data/synthetic-wave-source.js create mode 100644 src/data/websocket-source.js create mode 100644 src/demos.js create mode 100644 src/example-usage.js create mode 100644 src/main.js create mode 100644 src/metrics.js create mode 100644 src/plot-connections.js create mode 100644 src/plot/plot-buffer.js create mode 100644 src/plot/timeplot-view.js create mode 100644 src/state.js create mode 100644 src/styles.css create mode 100644 src/template-for-standard-site.js create mode 100644 src/test-data-generators.js create mode 100644 src/timeseries-plot.js create mode 100644 src/ui/panel-manager.js create mode 100644 src/utils-format.js create mode 100644 src/waterfall.js delete mode 100644 web-timeplot/.gitignore delete mode 100644 web-timeplot/AGENTS.md delete mode 100644 web-timeplot/ARCHITECTURE.md delete mode 100644 web-timeplot/PROTOTYPING.md delete mode 100644 web-timeplot/README.md delete mode 100644 web-timeplot/WEBSOCKET_FORMAT.md delete mode 100644 web-timeplot/bun.lock delete mode 100644 web-timeplot/index.html delete mode 100644 web-timeplot/package-lock.json delete mode 100644 web-timeplot/package.json delete mode 100644 web-timeplot/public/demo-data/chirp-ramp.csv delete mode 100644 web-timeplot/public/demo-data/step-bursts.csv delete mode 100644 web-timeplot/public/demo-data/telemetry-sweep.csv delete mode 100644 web-timeplot/scripts/demo-websocket-server.mjs delete mode 100644 web-timeplot/src/app/create-app.js delete mode 100644 web-timeplot/src/bootstrap.js delete mode 100644 web-timeplot/src/core/event-bus.js delete mode 100644 web-timeplot/src/core/store.js delete mode 100644 web-timeplot/src/core/time-controller.js delete mode 100644 web-timeplot/src/data-sources.js delete mode 100644 web-timeplot/src/data/base-source.js delete mode 100644 web-timeplot/src/data/csv-replay-source.js delete mode 100644 web-timeplot/src/data/parse-replay-csv.js delete mode 100644 web-timeplot/src/data/source-registry.js delete mode 100644 web-timeplot/src/data/synthetic-wave-source.js delete mode 100644 web-timeplot/src/data/websocket-source.js delete mode 100644 web-timeplot/src/demos.js delete mode 100644 web-timeplot/src/example-usage.js delete mode 100644 web-timeplot/src/main.js delete mode 100644 web-timeplot/src/metrics.js delete mode 100644 web-timeplot/src/plot-connections.js delete mode 100644 web-timeplot/src/plot/plot-buffer.js delete mode 100644 web-timeplot/src/plot/timeplot-view.js delete mode 100644 web-timeplot/src/state.js delete mode 100644 web-timeplot/src/styles.css delete mode 100644 web-timeplot/src/template-for-standard-site.js delete mode 100644 web-timeplot/src/test-data-generators.js delete mode 100644 web-timeplot/src/timeseries-plot.js delete mode 100644 web-timeplot/src/ui/panel-manager.js delete mode 100644 web-timeplot/src/utils-format.js delete mode 100644 web-timeplot/src/waterfall.js diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..e924e70 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(cargo build:*)" + ], + "deny": [], + "ask": [] + }, + "outputStyle": "Explanatory" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6d40cba..3c3629e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1 @@ -/rs/target -/cpp-timeplot/build/ -/web-timeplot/node_modules/ -/web-timeplot/dist/ +node_modules diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4926365 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,46 @@ +## Features to implement +### some of the features that i want to implement next + +- time speed adjustment +- pause and resume time +- label of current real time +- label of current plot time +- hover tooltip for data points +- data input system +- multiple panels that can be toggled on and off + - use extension scaffolding src as example https://github.com/MoebiusSolutions/extension-scaffold/tree/main + - possibly use ES directly, but ideally make own system if it can be better and simpler +- config panel + +## Systems - dividing up concerns in a sensible, performant, and easy-to-maintain-and-work-with way +### my initial thinking on the concerns of different architectural components. this very well may change, and each thing does not necessarily map to its own file or even its own class. + +- local configuration: read a config file +- state management: + - user preferences + - service config (websocket url, remote api endpoints) + - UI config (what panels acive, ) + - database stuff + - plots (which attributes of which input data-type are which graph axes, current time range viewable, ) + - data input stuff (structure of input datapoints, source, metadata) + - health (service connections, framerate, db access) + - +- application event system (to notify of state updates) +- rendering +- input data processing +- HID input handling + - input controller maps +- plot handling +- DB handling +- disk handling +- service handling (websockets, mqtt, HTTP REST) + +## Some important data structures used in the program + +- input actions: a set of "single action that the user can perform" +- user input: a single event of user input via hardware like key press or click or move joystick +- input action map: a map of "set of "user input"" => "input action" +- ui element definition: + +### Notes +- we will need a good system for state management, as the state will grow fairly large, including things like user preferences, UI config (what panels are displayed), \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..73c4cb6 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,194 @@ +# TimePlot Architecture + +## Overview + +The restarted TimePlot app is built around five small systems: + +1. **Store** — single source of truth for app state +2. **Time controller** — advances real time and plot time +3. **Source registry** — owns active data source lifecycle +4. **Plot view** — renders visible samples and handles hover picking +5. **Panel manager** — builds the DOM shell and user controls + +The current implementation is intentionally compact, but each system is already separated enough to grow without turning the app into a monolith again. + +Core workspace configuration is also persisted in `localStorage`, so plot settings, routing, and source setup survive reloads without persisting transient runtime state. + +## Runtime flow + +```text +TimeController.tick() + ↓ +Store.time updated + ↓ +SourceRegistry.update(plotTime) + ↓ +Synthetic / CSV replay / WebSocket sources emit samples + ↓ +PlotBuffer stores bounded history + ↓ +TimeplotView renders visible window + ↓ +PanelManager reflects status + tooltip +``` + +## Core principles + +### 1. Time is explicit + +Plot time is not inferred from frame count or rendering. It is advanced by `TimeController`, which makes features like pause, speed changes, replay, and stepping straightforward. + +### 2. Data sources are replaceable + +`SourceRegistry` talks to the active source through a tiny shared contract: + +- `start(startTimeMs)` +- `update(currentPlotTimeMs)` +- `reset(startTimeMs)` +- `updateConfig(partialConfig)` + +That keeps future WebSocket, file replay, database, or simulated sources easy to add. + +### 3. Rendering stays focused + +`TimeplotView` does not own application state or source orchestration. It receives state plus visible points and turns that into pixels. Hover detection also lives close to rendering because it depends on screen-space positions. + +### 4. UI panels stay in the DOM + +The plot is GPU-rendered with PixiJS. Controls, labels, and config panels stay in regular DOM so they are easy to iterate on, inspect, and restyle. + +### 5. Composition happens at the edge + +`create-app.js` is the composition root. It wires together store, time, sources, plot, UI, keyboard shortcuts, and the frame loop. That keeps the rest of the modules simple and testable. + +## Current state shape + +```js +{ + app: { title, renderer }, + time: { + realNowMs, + realElapsedMs, + plotTimeMs, + speed, + paused, + }, + plot: { + showGrid, + showPoints, + windowDurationMs, + maxPoints, + valueRange, + hoveredPoint, + tooltip, + }, + sources: { + signalA: { + type, + preset, + sampleRateHz, + amplitude, + noise, + replayRate, + wsUrl, + wsReconnectMs, + }, + signalB: { + ... + }, + }, + graphs: { + primary: { sourceKey, transform, title }, + secondary: { sourceKey, transform, title }, + }, + panels: { + status, + source, + config, + help, + }, +} +``` + +## Modules + +### `src/core/store.js` + +A tiny centralized store. It currently favors clarity over abstraction-heavy patterns. + +### `src/core/time-controller.js` + +Owns playback semantics: + +- pause/resume +- speed control +- plot time reset +- frame-to-frame delta handling + +### `src/data/synthetic-wave-source.js` + +Generates sample streams from a preset waveform. Right now it supports: + +- `telemetry` +- `chirp` +- `burst` + +### `src/data/csv-replay-source.js` + +Replays uploaded CSV datasets on the shared plot timebase. + +### `src/data/websocket-source.js` + +Streams live samples from a WebSocket server and reconnects automatically. + +### `src/plot/plot-buffer.js` + +Maintains bounded history so rendering and hover picking only operate on a manageable number of samples. + +### `src/plot/timeplot-view.js` + +Owns Pixi initialization, plotting, grid drawing, and nearest-point hover selection. + +### `src/ui/panel-manager.js` + +Creates: + +- top transport bar +- panel toggle buttons +- status panel +- data source panel +- config panel +- help panel +- floating tooltip + +## Why this is a better baseline + +The old project had useful ideas but too many concerns were mixed together. The new baseline is better because: + +- transport logic is separate from rendering +- data generation is separate from app wiring +- UI is separate from GPU drawing +- state is centralized and observable +- persisted configuration is separated from transient runtime state +- adding a new source or panel no longer requires rewriting the whole app + +## Recommended next steps + +### Near term + +- add persisted settings for panel visibility and playback preferences +- support multiple plot panes from a shared timebase +- add line/series definitions instead of a single hard-coded signal + +### Medium term + +- add schema-aware input adapters +- add WebSocket and replay-file sources +- add panel docking/layout persistence +- add markers, cursors, and annotations + +### Longer term + +- multi-stream synchronization +- richer interaction model for HID input mapping +- plug-in style source and panel registration diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/PROTOTYPING.md b/PROTOTYPING.md new file mode 100644 index 0000000..e220f9a --- /dev/null +++ b/PROTOTYPING.md @@ -0,0 +1,146 @@ +# PixiJS Prototyping Framework + +A minimal PixiJS framework with core architecture patterns (DOM initialization, Service initialization, State management) for rapid prototyping. + +## Quick Start + +```bash +npm run dev +``` + +Open browser to `http://localhost:5173/` + +## Architecture + +The framework follows a clean initialization pattern: + +1. **DOM Initialization** - Reference DOM elements +2. **Renderer Initialization** - Set up PixiJS with WebGPU/WebGL +3. **Services Initialization** - Start the update loop + +## Global Objects (Available in Console) + +- `window.PIXI` - Complete PixiJS namespace +- `window.pixiApp` - PixiJS Application instance +- `window.state` - StateManager instance (reactive state) +- `window.log` - Logger function + +## Rapid Prototyping Examples + +### Example 1: Draw a Rectangle + +Open browser console: + +```javascript +const graphics = new PIXI.Graphics(); +graphics.rect(100, 100, 200, 150); +graphics.fill(0xff0000); +pixiApp.stage.addChild(graphics); +``` + +### Example 2: Animated Sprite + +```javascript +const graphics = new PIXI.Graphics(); +graphics.circle(0, 0, 50); +graphics.fill(0x00ff00); +pixiApp.stage.addChild(graphics); + +// Add to update loop in main.js: +// graphics.x = Math.sin(state.state.time.current) * 200 + pixiApp.screen.width / 2; +// graphics.y = pixiApp.screen.height / 2; +``` + +### Example 3: Using State System + +The framework includes a reactive state manager: + +```javascript +// Listen to state changes +state.on('time.current', ({ value }) => { + console.log('Time:', value); +}); + +// Modify state (triggers listeners) +state.state.time.speed = 2.0; // Double speed + +// Toggle pause +state.togglePause(); +``` + +### Example 4: Register Input Actions + +```javascript +// In main.js, add to setupControls(): +state.registerAction('myAction', () => { + log('Action triggered!'); +}); + +state.mapKey('KeyP', 'myAction'); +``` + +## Modifying the Update Loop + +Edit `/src/main.js` function `update()`: + +```javascript +function update() { + state.incrementTime(0.016); // ~60fps increment + state.updateRealElapsed(); + state.state.rendering.frameCounter++; + + // YOUR PROTOTYPE CODE GOES HERE + // Example: + mySprite.rotation += 0.01; + myGraphics.x = Math.sin(state.state.time.current) * 100; +} +``` + +## State Structure + +```javascript +state.state = { + userPrefs: { + showGrid: true, + showMetrics: true, + theme: 'dark', + // ... persisted to localStorage + }, + + uiConfig: { + canvasWidth: number, + canvasHeight: number, + // ... + }, + + time: { + current: number, // Increments every frame + realElapsed: number, // Real seconds since start + speed: number, // Time multiplier + isPaused: boolean, + }, + + rendering: { + rendererType: 'webgpu' | 'webgl', + frameCounter: number, + }, + + health: { + fps: number, + updateMs: number, + renderMs: number, + }, +} +``` + +## Tips + +1. **Use the console** - All major objects are exposed globally +2. **Hot reload** - Vite will automatically reload on file changes +3. **State persistence** - userPrefs automatically save to localStorage +4. **Responsive** - Canvas automatically resizes with window +5. **WebGPU fallback** - Automatically falls back to WebGL if WebGPU unavailable + +## Clean Slate + +The framework intentionally draws nothing by default. Start adding your PixiJS objects and see results immediately. diff --git a/README.md b/README.md new file mode 100644 index 0000000..25dfb80 --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# TimePlot + +TimePlot is now a clean restart: a small PixiJS time-series sandbox built around a simple state core, a pluggable data source layer, and toggleable UI panels. + +## What it does + +- Real-time scrolling plot with PixiJS +- Pause/resume plot time +- Adjustable playback speed +- Current real-time and plot-time labels +- Hover tooltip for data points +- Modular synthetic data input system +- CSV replay sources +- WebSocket live sources +- Persisted workspace settings +- Toggleable side panels for status, source config, app config, and help + +## Getting started + +```bash +bun install +bun run dev +``` + +Production build: + +```bash +bun run build +bun run preview +``` + +Demo WebSocket source: + +```bash +bun run ws:demo +``` + +## Controls + +- `Space` — pause/resume +- `[` — slow down playback +- `]` — speed up playback +- `G` — toggle grid +- Hover plot — inspect nearest sample + +## Demo data + +Sample CSV replay files are included in [public/demo-data](public/demo-data): + +- [public/demo-data/telemetry-sweep.csv](public/demo-data/telemetry-sweep.csv) +- [public/demo-data/chirp-ramp.csv](public/demo-data/chirp-ramp.csv) +- [public/demo-data/step-bursts.csv](public/demo-data/step-bursts.csv) + +Use the `CSV replay` source type in the sidebar and upload one of those files. + +## WebSocket source + +TimePlot includes a local demo WebSocket server in [scripts/demo-websocket-server.mjs](scripts/demo-websocket-server.mjs). + +Start it with: + +```bash +bun run ws:demo +``` + +Then set a signal source to `WebSocket` and use `ws://localhost:8080`. + +Optional environment variables: + +```bash +PORT=8090 TIMEPLOT_PROFILE=chirp TIMEPLOT_INTERVAL_MS=50 bun run ws:demo +``` + +Supported demo profiles: + +- `telemetry` +- `chirp` +- `steps` +- `burst` + +Protocol details and accepted message formats are documented in [WEBSOCKET_FORMAT.md](WEBSOCKET_FORMAT.md). + +## Persistence + +TimePlot persists core workspace settings in `localStorage`, including: + +- plot display settings +- playback speed +- panel visibility +- graph routing and transforms +- source configuration such as presets and WebSocket URLs + +CSV replay files themselves are not persisted in storage. After a reload, TimePlot remembers which CSV file was selected but asks you to reload the file data. + +## Project structure + +```text +src/ +├── app/ +│ └── create-app.js # application composition root +├── core/ +│ ├── event-bus.js # lightweight pub/sub +│ ├── store.js # centralized app state +│ └── time-controller.js # real time + plot time transport +├── data/ +│ ├── base-source.js # source interface +│ ├── csv-replay-source.js +│ ├── parse-replay-csv.js +│ ├── source-registry.js # source lifecycle + routing +│ ├── synthetic-wave-source.js +│ └── websocket-source.js +├── plot/ +│ ├── plot-buffer.js # bounded in-memory sample history +│ └── timeplot-view.js # Pixi rendering + hover picking +├── ui/ +│ └── panel-manager.js # DOM shell, controls, panels, tooltip +├── bootstrap.js # startup entry +├── main.js # compatibility shim to bootstrap +├── styles.css # global UI styling +└── utils-format.js # display formatting helpers + +public/ +└── demo-data/ # sample CSV replay fixtures + +scripts/ +└── demo-websocket-server.mjs +``` + +## Design direction + +This restart intentionally optimizes for a strong foundation instead of feature sprawl: + +- transport and time are first-class systems +- data generation is isolated from rendering +- the plot owns visualization only +- DOM panels handle controls and diagnostics +- app composition happens in one predictable bootstrap path +- synthetic, file replay, and WebSocket sources share one source abstraction +- core workspace configuration survives reloads + +## Next good additions + +- richer external data sources (REST replay, binary streams, custom adapters) +- richer panel layout system with docking/persistence +- plot annotations and multiple stacked plots +- configurable schemas for incoming data types +- persistent user settings diff --git a/WEBSOCKET_FORMAT.md b/WEBSOCKET_FORMAT.md new file mode 100644 index 0000000..93eead2 --- /dev/null +++ b/WEBSOCKET_FORMAT.md @@ -0,0 +1,117 @@ +# WebSocket Data Format + +TimePlot's WebSocket source accepts UTF-8 text frames whose contents can be parsed into one of the supported payload shapes below. + +## Recommended payload + +Send one JSON object per message: + +```json +{ + "timestampMs": 1250, + "value": 0.482 +} +``` + +Fields: + +- `value` — required numeric sample value +- `timestampMs` — optional numeric source timestamp in milliseconds + +If `timestampMs` is present, TimePlot uses it to preserve the source timing relationship and aligns it onto the app's plot timebase. +If `timestampMs` is omitted, TimePlot stamps the sample at the current plot time when the message arrives. + +## Other accepted object keys + +TimePlot also accepts these alternate numeric field names: + +- value fields: `value`, `y`, `signal`, `data` +- time fields: `timeMs`, `timestampMs`, `timestamp`, `t` + +Examples: + +```json +{"y": 0.91, "t": 2040} +``` + +```json +{"signal": -0.13, "timestamp": 9810} +``` + +## Arrays + +A single message may contain an array of supported payloads: + +```json +[ + {"timestampMs": 1000, "value": 0.2}, + {"timestampMs": 1100, "value": 0.3}, + {"timestampMs": 1200, "value": 0.5} +] +``` + +This is useful for batching. + +## Bare numeric messages + +These also work, though JSON objects are preferred: + +```text +0.418 +``` + +or: + +```json +42.5 +``` + +These are treated as samples without an explicit timestamp. + +## Unsupported / ignored messages + +Messages are ignored if TimePlot cannot find a numeric sample value. +Examples of ignored payloads: + +- empty strings +- non-numeric strings +- JSON objects without a numeric `value`-like field + +## Demo server compatibility + +The included demo server sends messages like: + +```json +{ + "timestampMs": 1870, + "value": 0.735812, + "sequence": 19, + "profile": "telemetry" +} +``` + +Extra fields are safe. TimePlot ignores anything it does not need. + +## Running the demo server + +```bash +bun run ws:demo +``` + +Environment options: + +- `PORT` — default `8080` +- `TIMEPLOT_PROFILE` — `telemetry`, `chirp`, `steps`, or `burst` +- `TIMEPLOT_INTERVAL_MS` — message interval in milliseconds + +Example: + +```bash +PORT=8090 TIMEPLOT_PROFILE=chirp TIMEPLOT_INTERVAL_MS=50 bun run ws:demo +``` + +Then set a signal source type to `WebSocket` and point it at: + +```text +ws://localhost:8090 +``` diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..82f672d --- /dev/null +++ b/bun.lock @@ -0,0 +1,150 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "web-timeplot", + "dependencies": { + "pixi.js": "^8.0.0", + }, + "devDependencies": { + "vite": "^5.0.0", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@pixi/colord": ["@pixi/colord@2.9.6", "", {}, "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.3", "", { "os": "android", "cpu": "arm" }, "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.3", "", { "os": "android", "cpu": "arm64" }, "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.3", "", { "os": "linux", "cpu": "arm" }, "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.3", "", { "os": "linux", "cpu": "arm" }, "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.3", "", { "os": "linux", "cpu": "none" }, "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.3", "", { "os": "linux", "cpu": "none" }, "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.3", "", { "os": "linux", "cpu": "none" }, "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.3", "", { "os": "linux", "cpu": "x64" }, "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.3", "", { "os": "none", "cpu": "arm64" }, "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.3", "", { "os": "win32", "cpu": "x64" }, "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.3", "", { "os": "win32", "cpu": "x64" }, "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA=="], + + "@types/css-font-loading-module": ["@types/css-font-loading-module@0.0.12", "", {}, "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA=="], + + "@types/earcut": ["@types/earcut@3.0.0", "", {}, "sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@webgpu/types": ["@webgpu/types@0.1.65", "", {}, "sha512-cYrHab4d6wuVvDW5tdsfI6/o6vcLMDe6w2Citd1oS51Xxu2ycLCnVo4fqwujfKWijrZMInTJIKcXxteoy21nVA=="], + + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], + + "earcut": ["earcut@3.0.2", "", {}, "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ=="], + + "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": "bin/esbuild" }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gifuct-js": ["gifuct-js@2.1.2", "", { "dependencies": { "js-binary-schema-parser": "^2.0.3" } }, "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg=="], + + "ismobilejs": ["ismobilejs@1.1.1", "", {}, "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw=="], + + "js-binary-schema-parser": ["js-binary-schema-parser@2.0.3", "", {}, "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "parse-svg-path": ["parse-svg-path@0.1.2", "", {}, "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "pixi.js": ["pixi.js@8.13.2", "", { "dependencies": { "@pixi/colord": "^2.9.6", "@types/css-font-loading-module": "^0.0.12", "@types/earcut": "^3.0.0", "@webgpu/types": "^0.1.40", "@xmldom/xmldom": "^0.8.10", "earcut": "^3.0.2", "eventemitter3": "^5.0.1", "gifuct-js": "^2.1.2", "ismobilejs": "^1.1.1", "parse-svg-path": "^0.1.2", "tiny-lru": "^11.4.5" } }, "sha512-9KVGZ4a99TA5SwUEWs9m5gliX6XUCS1aGc/DOPsXxpqLMDRa+FhzpT5ao9z1UwLYJkSvt3rcQs+aZXECBHSSHg=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "rollup": ["rollup@4.52.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.3", "@rollup/rollup-android-arm64": "4.52.3", "@rollup/rollup-darwin-arm64": "4.52.3", "@rollup/rollup-darwin-x64": "4.52.3", "@rollup/rollup-freebsd-arm64": "4.52.3", "@rollup/rollup-freebsd-x64": "4.52.3", "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", "@rollup/rollup-linux-arm-musleabihf": "4.52.3", "@rollup/rollup-linux-arm64-gnu": "4.52.3", "@rollup/rollup-linux-arm64-musl": "4.52.3", "@rollup/rollup-linux-loong64-gnu": "4.52.3", "@rollup/rollup-linux-ppc64-gnu": "4.52.3", "@rollup/rollup-linux-riscv64-gnu": "4.52.3", "@rollup/rollup-linux-riscv64-musl": "4.52.3", "@rollup/rollup-linux-s390x-gnu": "4.52.3", "@rollup/rollup-linux-x64-gnu": "4.52.3", "@rollup/rollup-linux-x64-musl": "4.52.3", "@rollup/rollup-openharmony-arm64": "4.52.3", "@rollup/rollup-win32-arm64-msvc": "4.52.3", "@rollup/rollup-win32-ia32-msvc": "4.52.3", "@rollup/rollup-win32-x64-gnu": "4.52.3", "@rollup/rollup-win32-x64-msvc": "4.52.3", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "tiny-lru": ["tiny-lru@11.4.5", "", {}, "sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw=="], + + "vite": ["vite@5.4.20", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g=="], + } +} diff --git a/data1.txt b/data1.txt new file mode 100644 index 0000000..4a8d5d3 --- /dev/null +++ b/data1.txt @@ -0,0 +1,50 @@ +0.1 +0.3 +0.5 +0.7 +0.9 +1.1 +1.3 +1.5 +1.7 +1.9 +2.1 +2.3 +2.5 +2.7 +2.9 +3.1 +3.3 +3.5 +3.7 +3.9 +4.1 +4.3 +4.5 +4.7 +4.9 +5.0 +4.8 +4.6 +4.4 +4.2 +4.0 +3.8 +3.6 +3.4 +3.2 +3.0 +2.8 +2.6 +2.4 +2.2 +2.0 +1.8 +1.6 +1.4 +1.2 +1.0 +0.8 +0.6 +0.4 +0.2 \ No newline at end of file diff --git a/data2.txt b/data2.txt new file mode 100644 index 0000000..f4772c0 --- /dev/null +++ b/data2.txt @@ -0,0 +1,50 @@ +1.0 +1.5 +0.8 +2.1 +0.3 +2.8 +0.1 +3.2 +0.6 +1.9 +0.4 +2.5 +1.2 +0.9 +3.0 +0.7 +1.8 +2.3 +0.5 +2.9 +1.1 +0.2 +2.7 +1.4 +0.8 +2.1 +1.6 +0.3 +2.4 +1.3 +0.6 +2.8 +0.9 +1.7 +2.2 +0.4 +2.6 +1.0 +0.7 +2.0 +1.5 +0.1 +2.3 +1.8 +0.5 +2.5 +1.2 +0.8 +2.9 +0.3 \ No newline at end of file diff --git a/data3.txt b/data3.txt new file mode 100644 index 0000000..7b8e980 --- /dev/null +++ b/data3.txt @@ -0,0 +1,50 @@ +2.0 +2.05 +2.1 +2.08 +2.15 +2.12 +2.18 +2.16 +2.22 +2.19 +2.25 +2.23 +2.28 +2.26 +2.31 +2.29 +2.34 +2.32 +2.37 +2.35 +2.40 +2.38 +2.43 +2.41 +2.46 +2.44 +2.49 +2.47 +2.52 +2.50 +2.55 +2.53 +2.58 +2.56 +2.61 +2.59 +2.64 +2.62 +2.67 +2.65 +2.70 +2.68 +2.73 +2.71 +2.76 +2.74 +2.79 +2.77 +2.82 +2.80 \ No newline at end of file diff --git a/data4.txt b/data4.txt new file mode 100644 index 0000000..8b1b855 --- /dev/null +++ b/data4.txt @@ -0,0 +1,30 @@ +{"timestamp": 1700000000, "value": 0.5} +{"timestamp": 1700000001, "value": 1.2} +{"timestamp": 1700000002, "value": 0.8} +{"timestamp": 1700000003, "value": 2.1} +{"timestamp": 1700000004, "value": 1.5} +{"timestamp": 1700000005, "value": 0.3} +{"timestamp": 1700000006, "value": 1.9} +{"timestamp": 1700000007, "value": 0.7} +{"timestamp": 1700000008, "value": 2.3} +{"timestamp": 1700000009, "value": 1.1} +{"timestamp": 1700000010, "value": 0.4} +{"timestamp": 1700000011, "value": 1.8} +{"timestamp": 1700000012, "value": 0.9} +{"timestamp": 1700000013, "value": 2.0} +{"timestamp": 1700000014, "value": 1.4} +{"timestamp": 1700000015, "value": 0.6} +{"timestamp": 1700000016, "value": 1.7} +{"timestamp": 1700000017, "value": 0.2} +{"timestamp": 1700000018, "value": 2.2} +{"timestamp": 1700000019, "value": 1.3} +{"timestamp": 1700000020, "value": 0.1} +{"timestamp": 1700000021, "value": 1.6} +{"timestamp": 1700000022, "value": 0.8} +{"timestamp": 1700000023, "value": 2.4} +{"timestamp": 1700000024, "value": 1.0} +{"timestamp": 1700000025, "value": 0.5} +{"timestamp": 1700000026, "value": 1.9} +{"timestamp": 1700000027, "value": 0.3} +{"timestamp": 1700000028, "value": 2.1} +{"timestamp": 1700000029, "value": 1.2} \ No newline at end of file diff --git a/dist/assets/SharedSystems-DVK37F7d.js b/dist/assets/SharedSystems-DVK37F7d.js new file mode 100644 index 0000000..5d71c0c --- /dev/null +++ b/dist/assets/SharedSystems-DVK37F7d.js @@ -0,0 +1,326 @@ +import{q as He,G as ne,t as ie,u as ze,k as oe,M as v,v as le,E as u,e as O,x as S,y as We,z as F,F as b,R as L,H as ue,I as Ve,s as m,S as f,h as B,w as H,J,K as Ne,b as X,B as k,i as U,L as je,N as M,j as T,O as w,Q as $e,a as qe,V as de,W as ce,X as he,Y as fe,C as P,Z as Ke,_ as A,$ as Q,D as z,a0 as Ye,P as Je,c as Xe,T as Z,a1 as ee,a2 as Qe,a3 as Ze,a4 as et}from"./index-DB6B0XuI.js";import{S as pe,B as me,c as tt}from"./colorToUniform-DmtBy-2V.js";const ve=class I extends He{constructor(e){e={...I.defaultOptions,...e},super(e),this.enabled=!0,this._state=pe.for2d(),this.blendMode=e.blendMode,this.padding=e.padding,typeof e.antialias=="boolean"?this.antialias=e.antialias?"on":"off":this.antialias=e.antialias,this.resolution=e.resolution,this.blendRequired=e.blendRequired,this.clipToViewport=e.clipToViewport,this.addResource("uTexture",0,1)}apply(e,t,r,s){e.applyFilter(this,t,r,s)}get blendMode(){return this._state.blendMode}set blendMode(e){this._state.blendMode=e}static from(e){const{gpu:t,gl:r,...s}=e;let a,i;return t&&(a=ne.from(t)),r&&(i=ie.from(r)),new I({gpuProgram:a,glProgram:i,...s})}};ve.defaultOptions={blendMode:"normal",resolution:1,padding:0,antialias:"off",blendRequired:!1,clipToViewport:!0};let rt=ve;var st=`in vec2 vMaskCoord; +in vec2 vTextureCoord; + +uniform sampler2D uTexture; +uniform sampler2D uMaskTexture; + +uniform float uAlpha; +uniform vec4 uMaskClamp; +uniform float uInverse; + +out vec4 finalColor; + +void main(void) +{ + float clip = step(3.5, + step(uMaskClamp.x, vMaskCoord.x) + + step(uMaskClamp.y, vMaskCoord.y) + + step(vMaskCoord.x, uMaskClamp.z) + + step(vMaskCoord.y, uMaskClamp.w)); + + // TODO look into why this is needed + float npmAlpha = uAlpha; + vec4 original = texture(uTexture, vTextureCoord); + vec4 masky = texture(uMaskTexture, vMaskCoord); + float alphaMul = 1.0 - npmAlpha * (1.0 - masky.a); + + float a = alphaMul * masky.r * npmAlpha * clip; + + if (uInverse == 1.0) { + a = 1.0 - a; + } + + finalColor = original * a; +} +`,at=`in vec2 aPosition; + +out vec2 vTextureCoord; +out vec2 vMaskCoord; + + +uniform vec4 uInputSize; +uniform vec4 uOutputFrame; +uniform vec4 uOutputTexture; +uniform mat3 uFilterMatrix; + +vec4 filterVertexPosition( vec2 aPosition ) +{ + vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy; + + position.x = position.x * (2.0 / uOutputTexture.x) - 1.0; + position.y = position.y * (2.0*uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z; + + return vec4(position, 0.0, 1.0); +} + +vec2 filterTextureCoord( vec2 aPosition ) +{ + return aPosition * (uOutputFrame.zw * uInputSize.zw); +} + +vec2 getFilterCoord( vec2 aPosition ) +{ + return ( uFilterMatrix * vec3( filterTextureCoord(aPosition), 1.0) ).xy; +} + +void main(void) +{ + gl_Position = filterVertexPosition(aPosition); + vTextureCoord = filterTextureCoord(aPosition); + vMaskCoord = getFilterCoord(aPosition); +} +`,te=`struct GlobalFilterUniforms { + uInputSize:vec4, + uInputPixel:vec4, + uInputClamp:vec4, + uOutputFrame:vec4, + uGlobalFrame:vec4, + uOutputTexture:vec4, +}; + +struct MaskUniforms { + uFilterMatrix:mat3x3, + uMaskClamp:vec4, + uAlpha:f32, + uInverse:f32, +}; + +@group(0) @binding(0) var gfu: GlobalFilterUniforms; +@group(0) @binding(1) var uTexture: texture_2d; +@group(0) @binding(2) var uSampler : sampler; + +@group(1) @binding(0) var filterUniforms : MaskUniforms; +@group(1) @binding(1) var uMaskTexture: texture_2d; + +struct VSOutput { + @builtin(position) position: vec4, + @location(0) uv : vec2, + @location(1) filterUv : vec2, +}; + +fn filterVertexPosition(aPosition:vec2) -> vec4 +{ + var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy; + + position.x = position.x * (2.0 / gfu.uOutputTexture.x) - 1.0; + position.y = position.y * (2.0*gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z; + + return vec4(position, 0.0, 1.0); +} + +fn filterTextureCoord( aPosition:vec2 ) -> vec2 +{ + return aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw); +} + +fn globalTextureCoord( aPosition:vec2 ) -> vec2 +{ + return (aPosition.xy / gfu.uGlobalFrame.zw) + (gfu.uGlobalFrame.xy / gfu.uGlobalFrame.zw); +} + +fn getFilterCoord(aPosition:vec2 ) -> vec2 +{ + return ( filterUniforms.uFilterMatrix * vec3( filterTextureCoord(aPosition), 1.0) ).xy; +} + +fn getSize() -> vec2 +{ + return gfu.uGlobalFrame.zw; +} + +@vertex +fn mainVertex( + @location(0) aPosition : vec2, +) -> VSOutput { + return VSOutput( + filterVertexPosition(aPosition), + filterTextureCoord(aPosition), + getFilterCoord(aPosition) + ); +} + +@fragment +fn mainFragment( + @location(0) uv: vec2, + @location(1) filterUv: vec2, + @builtin(position) position: vec4 +) -> @location(0) vec4 { + + var maskClamp = filterUniforms.uMaskClamp; + var uAlpha = filterUniforms.uAlpha; + + var clip = step(3.5, + step(maskClamp.x, filterUv.x) + + step(maskClamp.y, filterUv.y) + + step(filterUv.x, maskClamp.z) + + step(filterUv.y, maskClamp.w)); + + var mask = textureSample(uMaskTexture, uSampler, filterUv); + var source = textureSample(uTexture, uSampler, uv); + var alphaMul = 1.0 - uAlpha * (1.0 - mask.a); + + var a: f32 = alphaMul * mask.r * uAlpha * clip; + + if (filterUniforms.uInverse == 1.0) { + a = 1.0 - a; + } + + return source * a; +} +`;class nt extends rt{constructor(e){const{sprite:t,...r}=e,s=new ze(t.texture),a=new oe({uFilterMatrix:{value:new v,type:"mat3x3"},uMaskClamp:{value:s.uClampFrame,type:"vec4"},uAlpha:{value:1,type:"f32"},uInverse:{value:e.inverse?1:0,type:"f32"}}),i=ne.from({vertex:{source:te,entryPoint:"mainVertex"},fragment:{source:te,entryPoint:"mainFragment"}}),o=ie.from({vertex:at,fragment:st,name:"mask-filter"});super({...r,gpuProgram:i,glProgram:o,clipToViewport:!1,resources:{filterUniforms:a,uMaskTexture:t.texture.source}}),this.sprite=t,this._textureMatrix=s}set inverse(e){this.resources.filterUniforms.uniforms.uInverse=e?1:0}get inverse(){return this.resources.filterUniforms.uniforms.uInverse===1}apply(e,t,r,s){this._textureMatrix.texture=this.sprite.texture,e.calculateSpriteMatrix(this.resources.filterUniforms.uniforms.uFilterMatrix,this.sprite).prepend(this._textureMatrix.mapCoord),this.resources.uMaskTexture=this.sprite.texture.source,e.applyFilter(this,t,r,s)}}const W=class ge{constructor(e,t){var r,s;this.state=pe.for2d(),this._batchersByInstructionSet=Object.create(null),this._activeBatches=Object.create(null),this.renderer=e,this._adaptor=t,(s=(r=this._adaptor).init)==null||s.call(r,this)}static getBatcher(e){return new this._availableBatchers[e]}buildStart(e){let t=this._batchersByInstructionSet[e.uid];t||(t=this._batchersByInstructionSet[e.uid]=Object.create(null),t.default||(t.default=new le({maxTextures:this.renderer.limits.maxBatchableTextures}))),this._activeBatches=t,this._activeBatch=this._activeBatches.default;for(const r in this._activeBatches)this._activeBatches[r].begin()}addToBatch(e,t){if(this._activeBatch.name!==e.batcherName){this._activeBatch.break(t);let r=this._activeBatches[e.batcherName];r||(r=this._activeBatches[e.batcherName]=ge.getBatcher(e.batcherName),r.begin()),this._activeBatch=r}this._activeBatch.add(e)}break(e){this._activeBatch.break(e)}buildEnd(e){this._activeBatch.break(e);const t=this._activeBatches;for(const r in t){const s=t[r],a=s.geometry;a.indexBuffer.setDataWithSize(s.indexBuffer,s.indexSize,!0),a.buffers[0].setDataWithSize(s.attributeBuffer.float32View,s.attributeSize,!1)}}upload(e){const t=this._batchersByInstructionSet[e.uid];for(const r in t){const s=t[r],a=s.geometry;s.dirty&&(s.dirty=!1,a.buffers[0].update(s.attributeSize*4))}}execute(e){if(e.action==="startBatch"){const t=e.batcher,r=t.geometry,s=t.shader;this._adaptor.start(this,r,s)}this._adaptor.execute(this,e)}destroy(){this.state=null,this.renderer=null,this._adaptor=null;for(const e in this._activeBatches)this._activeBatches[e].destroy();this._activeBatches=null}};W.extension={type:[u.WebGLPipes,u.WebGPUPipes,u.CanvasPipes],name:"batch"};W._availableBatchers=Object.create(null);let xe=W;O.handleByMap(u.Batcher,xe._availableBatchers);O.add(le);const It={name:"texture-bit",vertex:{header:` + + struct TextureUniforms { + uTextureMatrix:mat3x3, + } + + @group(2) @binding(2) var textureUniforms : TextureUniforms; + `,main:` + uv = (textureUniforms.uTextureMatrix * vec3(uv, 1.0)).xy; + `},fragment:{header:` + @group(2) @binding(0) var uTexture: texture_2d; + @group(2) @binding(1) var uSampler: sampler; + + + `,main:` + outColor = textureSample(uTexture, uSampler, vUV); + `}},Gt={name:"texture-bit",vertex:{header:` + uniform mat3 uTextureMatrix; + `,main:` + uv = (uTextureMatrix * vec3(uv, 1.0)).xy; + `},fragment:{header:` + uniform sampler2D uTexture; + + + `,main:` + outColor = texture(uTexture, vUV); + `}},it=new F;class ot extends ue{constructor(){super(),this.filters=[new nt({sprite:new Ve(m.EMPTY),inverse:!1,resolution:"inherit",antialias:"inherit"})]}get sprite(){return this.filters[0].sprite}set sprite(e){this.filters[0].sprite=e}get inverse(){return this.filters[0].inverse}set inverse(e){this.filters[0].inverse=e}}class _e{constructor(e){this._activeMaskStage=[],this._renderer=e}push(e,t,r){const s=this._renderer;if(s.renderPipes.batch.break(r),r.add({renderPipeId:"alphaMask",action:"pushMaskBegin",mask:e,inverse:t._maskOptions.inverse,canBundle:!1,maskedContainer:t}),e.inverse=t._maskOptions.inverse,e.renderMaskToTexture){const a=e.mask;a.includeInBuild=!0,a.collectRenderables(r,s,null),a.includeInBuild=!1}s.renderPipes.batch.break(r),r.add({renderPipeId:"alphaMask",action:"pushMaskEnd",mask:e,maskedContainer:t,inverse:t._maskOptions.inverse,canBundle:!1})}pop(e,t,r){this._renderer.renderPipes.batch.break(r),r.add({renderPipeId:"alphaMask",action:"popMaskEnd",mask:e,inverse:t._maskOptions.inverse,canBundle:!1})}execute(e){const t=this._renderer,r=e.mask.renderMaskToTexture;if(e.action==="pushMaskBegin"){const s=S.get(ot);if(s.inverse=e.inverse,r){e.mask.mask.measurable=!0;const a=We(e.mask.mask,!0,it);e.mask.mask.measurable=!1,a.ceil();const i=t.renderTarget.renderTarget.colorTexture.source,o=b.getOptimalTexture(a.width,a.height,i._resolution,i.antialias);t.renderTarget.push(o,!0),t.globalUniforms.push({offset:a,worldColor:4294967295});const l=s.sprite;l.texture=o,l.worldTransform.tx=a.minX,l.worldTransform.ty=a.minY,this._activeMaskStage.push({filterEffect:s,maskedContainer:e.maskedContainer,filterTexture:o})}else s.sprite=e.mask.mask,this._activeMaskStage.push({filterEffect:s,maskedContainer:e.maskedContainer})}else if(e.action==="pushMaskEnd"){const s=this._activeMaskStage[this._activeMaskStage.length-1];r&&(t.type===L.WEBGL&&t.renderTarget.finishRenderPass(),t.renderTarget.pop(),t.globalUniforms.pop()),t.filter.push({renderPipeId:"filter",action:"pushFilter",container:s.maskedContainer,filterEffect:s.filterEffect,canBundle:!1})}else if(e.action==="popMaskEnd"){t.filter.pop();const s=this._activeMaskStage.pop();r&&b.returnTexture(s.filterTexture),S.return(s.filterEffect)}}destroy(){this._renderer=null,this._activeMaskStage=null}}_e.extension={type:[u.WebGLPipes,u.WebGPUPipes,u.CanvasPipes],name:"alphaMask"};class be{constructor(e){this._colorStack=[],this._colorStackIndex=0,this._currentColor=0,this._renderer=e}buildStart(){this._colorStack[0]=15,this._colorStackIndex=1,this._currentColor=15}push(e,t,r){this._renderer.renderPipes.batch.break(r);const a=this._colorStack;a[this._colorStackIndex]=a[this._colorStackIndex-1]&e.mask;const i=this._colorStack[this._colorStackIndex];i!==this._currentColor&&(this._currentColor=i,r.add({renderPipeId:"colorMask",colorMask:i,canBundle:!1})),this._colorStackIndex++}pop(e,t,r){this._renderer.renderPipes.batch.break(r);const a=this._colorStack;this._colorStackIndex--;const i=a[this._colorStackIndex-1];i!==this._currentColor&&(this._currentColor=i,r.add({renderPipeId:"colorMask",colorMask:i,canBundle:!1}))}execute(e){this._renderer.colorMask.setMask(e.colorMask)}destroy(){this._renderer=null,this._colorStack=null}}be.extension={type:[u.WebGLPipes,u.WebGPUPipes,u.CanvasPipes],name:"colorMask"};class Te{constructor(e){this._maskStackHash={},this._maskHash=new WeakMap,this._renderer=e}push(e,t,r){var s;const a=e,i=this._renderer;i.renderPipes.batch.break(r),i.renderPipes.blendMode.setBlendMode(a.mask,"none",r),r.add({renderPipeId:"stencilMask",action:"pushMaskBegin",mask:e,inverse:t._maskOptions.inverse,canBundle:!1});const o=a.mask;o.includeInBuild=!0,this._maskHash.has(a)||this._maskHash.set(a,{instructionsStart:0,instructionsLength:0});const l=this._maskHash.get(a);l.instructionsStart=r.instructionSize,o.collectRenderables(r,i,null),o.includeInBuild=!1,i.renderPipes.batch.break(r),r.add({renderPipeId:"stencilMask",action:"pushMaskEnd",mask:e,inverse:t._maskOptions.inverse,canBundle:!1});const d=r.instructionSize-l.instructionsStart-1;l.instructionsLength=d;const c=i.renderTarget.renderTarget.uid;(s=this._maskStackHash)[c]??(s[c]=0)}pop(e,t,r){const s=e,a=this._renderer;a.renderPipes.batch.break(r),a.renderPipes.blendMode.setBlendMode(s.mask,"none",r),r.add({renderPipeId:"stencilMask",action:"popMaskBegin",inverse:t._maskOptions.inverse,canBundle:!1});const i=this._maskHash.get(e);for(let o=0;oe.uniformStructures[o]),a=this._adaptor.createUboElements(s),i=this._generateUboSync(a.uboElements);r=this._syncFunctionHash[t]={layout:a,syncFunction:i}}return this._syncFunctionHash[t]}_generateUboSync(e){return this._adaptor.generateUboSync(e)}syncUniformGroup(e,t,r){const s=this.getUniformGroupData(e);e.buffer||(e.buffer=new X({data:new Float32Array(s.layout.size/4),usage:k.UNIFORM|k.COPY_DST}));let a=null;return t||(t=e.buffer.data,a=e.buffer.dataInt32),r||(r=0),s.syncFunction(e.uniforms,t,a,r),!0}updateUniformGroup(e){if(e.isStatic&&!e._dirtyId)return!1;e._dirtyId=0;const t=this.syncUniformGroup(e);return e.buffer.update(),t}destroy(){this._syncFunctionHash=null}}const C=[{type:"mat3x3",test:n=>n.value.a!==void 0,ubo:` + var matrix = uv[name].toArray(true); + data[offset] = matrix[0]; + data[offset + 1] = matrix[1]; + data[offset + 2] = matrix[2]; + data[offset + 4] = matrix[3]; + data[offset + 5] = matrix[4]; + data[offset + 6] = matrix[5]; + data[offset + 8] = matrix[6]; + data[offset + 9] = matrix[7]; + data[offset + 10] = matrix[8]; + `,uniform:` + gl.uniformMatrix3fv(ud[name].location, false, uv[name].toArray(true)); + `},{type:"vec4",test:n=>n.type==="vec4"&&n.size===1&&n.value.width!==void 0,ubo:` + v = uv[name]; + data[offset] = v.x; + data[offset + 1] = v.y; + data[offset + 2] = v.width; + data[offset + 3] = v.height; + `,uniform:` + cv = ud[name].value; + v = uv[name]; + if (cv[0] !== v.x || cv[1] !== v.y || cv[2] !== v.width || cv[3] !== v.height) { + cv[0] = v.x; + cv[1] = v.y; + cv[2] = v.width; + cv[3] = v.height; + gl.uniform4f(ud[name].location, v.x, v.y, v.width, v.height); + } + `},{type:"vec2",test:n=>n.type==="vec2"&&n.size===1&&n.value.x!==void 0,ubo:` + v = uv[name]; + data[offset] = v.x; + data[offset + 1] = v.y; + `,uniform:` + cv = ud[name].value; + v = uv[name]; + if (cv[0] !== v.x || cv[1] !== v.y) { + cv[0] = v.x; + cv[1] = v.y; + gl.uniform2f(ud[name].location, v.x, v.y); + } + `},{type:"vec4",test:n=>n.type==="vec4"&&n.size===1&&n.value.red!==void 0,ubo:` + v = uv[name]; + data[offset] = v.red; + data[offset + 1] = v.green; + data[offset + 2] = v.blue; + data[offset + 3] = v.alpha; + `,uniform:` + cv = ud[name].value; + v = uv[name]; + if (cv[0] !== v.red || cv[1] !== v.green || cv[2] !== v.blue || cv[3] !== v.alpha) { + cv[0] = v.red; + cv[1] = v.green; + cv[2] = v.blue; + cv[3] = v.alpha; + gl.uniform4f(ud[name].location, v.red, v.green, v.blue, v.alpha); + } + `},{type:"vec3",test:n=>n.type==="vec3"&&n.size===1&&n.value.red!==void 0,ubo:` + v = uv[name]; + data[offset] = v.red; + data[offset + 1] = v.green; + data[offset + 2] = v.blue; + `,uniform:` + cv = ud[name].value; + v = uv[name]; + if (cv[0] !== v.red || cv[1] !== v.green || cv[2] !== v.blue) { + cv[0] = v.red; + cv[1] = v.green; + cv[2] = v.blue; + gl.uniform3f(ud[name].location, v.red, v.green, v.blue); + } + `}];function Ot(n,e,t,r){const s=[` + var v = null; + var v2 = null; + var t = 0; + var index = 0; + var name = null; + var arrayOffset = null; + `];let a=0;for(let o=0;o1)h=l.offset/4,s.push(t(l,h-a));else{const p=r[l.data.type];h=l.offset/4,s.push(` + v = uv.${d}; + offset += ${h-a}; + ${p}; + `)}a=h}const i=s.join(` +`);return new Function("uv","data","dataInt32","offset",i)}function g(n,e){return` + for (let i = 0; i < ${n*e}; i++) { + data[offset + (((i / ${n})|0) * 4) + (i % ${n})] = v[i]; + } + `}const ut={f32:` + data[offset] = v;`,i32:` + dataInt32[offset] = v;`,"vec2":` + data[offset] = v[0]; + data[offset + 1] = v[1];`,"vec3":` + data[offset] = v[0]; + data[offset + 1] = v[1]; + data[offset + 2] = v[2];`,"vec4":` + data[offset] = v[0]; + data[offset + 1] = v[1]; + data[offset + 2] = v[2]; + data[offset + 3] = v[3];`,"vec2":` + dataInt32[offset] = v[0]; + dataInt32[offset + 1] = v[1];`,"vec3":` + dataInt32[offset] = v[0]; + dataInt32[offset + 1] = v[1]; + dataInt32[offset + 2] = v[2];`,"vec4":` + dataInt32[offset] = v[0]; + dataInt32[offset + 1] = v[1]; + dataInt32[offset + 2] = v[2]; + dataInt32[offset + 3] = v[3];`,"mat2x2":` + data[offset] = v[0]; + data[offset + 1] = v[1]; + data[offset + 4] = v[2]; + data[offset + 5] = v[3];`,"mat3x3":` + data[offset] = v[0]; + data[offset + 1] = v[1]; + data[offset + 2] = v[2]; + data[offset + 4] = v[3]; + data[offset + 5] = v[4]; + data[offset + 6] = v[5]; + data[offset + 8] = v[6]; + data[offset + 9] = v[7]; + data[offset + 10] = v[8];`,"mat4x4":` + for (let i = 0; i < 16; i++) { + data[offset + i] = v[i]; + }`,"mat3x2":g(3,2),"mat4x2":g(4,2),"mat2x3":g(2,3),"mat4x3":g(4,3),"mat2x4":g(2,4),"mat3x4":g(3,4)},Ft={...ut,"mat2x2":` + data[offset] = v[0]; + data[offset + 1] = v[1]; + data[offset + 2] = v[2]; + data[offset + 3] = v[3]; + `};function dt(n,e,t,r,s,a){const i=a?1:-1;return n.identity(),n.a=1/r*2,n.d=i*(1/s*2),n.tx=-1-e*n.a,n.ty=-i-t*n.d,n}const x=new Map;je.register(x);function ye(n,e){if(!x.has(n)){const t=new m({source:new U({resource:n,...e})}),r=()=>{x.get(n)===t&&x.delete(n)};t.once("destroy",r),t.source.once("destroy",r),x.set(n,t)}return x.get(n)}function ct(n){const e=n.colorTexture.source.resource;return globalThis.HTMLCanvasElement&&e instanceof HTMLCanvasElement&&document.body.contains(e)}const ke=class Ce{constructor(e={}){if(this.uid=M("renderTarget"),this.colorTextures=[],this.dirtyId=0,this.isRoot=!1,this._size=new Float32Array(2),this._managedColorTextures=!1,e={...Ce.defaultOptions,...e},this.stencil=e.stencil,this.depth=e.depth,this.isRoot=e.isRoot,typeof e.colorTextures=="number"){this._managedColorTextures=!0;for(let t=0;tr.source)];const t=this.colorTexture.source;this.resize(t.width,t.height,t._resolution)}this.colorTexture.source.on("resize",this.onSourceResize,this),(e.depthStencilTexture||this.stencil)&&(e.depthStencilTexture instanceof m||e.depthStencilTexture instanceof T?this.depthStencilTexture=e.depthStencilTexture.source:this.ensureDepthStencilTexture())}get size(){const e=this._size;return e[0]=this.pixelWidth,e[1]=this.pixelHeight,e}get width(){return this.colorTexture.source.width}get height(){return this.colorTexture.source.height}get pixelWidth(){return this.colorTexture.source.pixelWidth}get pixelHeight(){return this.colorTexture.source.pixelHeight}get resolution(){return this.colorTexture.source._resolution}get colorTexture(){return this.colorTextures[0]}onSourceResize(e){this.resize(e.width,e.height,e._resolution,!0)}ensureDepthStencilTexture(){this.depthStencilTexture||(this.depthStencilTexture=new T({width:this.width,height:this.height,resolution:this.resolution,format:"depth24plus-stencil8",autoGenerateMipmaps:!1,antialias:!1,mipLevelCount:1}))}resize(e,t,r=this.resolution,s=!1){this.dirtyId++,this.colorTextures.forEach((a,i)=>{s&&i===0||a.source.resize(e,t,r)}),this.depthStencilTexture&&this.depthStencilTexture.source.resize(e,t,r)}destroy(){this.colorTexture.source.off("resize",this.onSourceResize,this),this._managedColorTextures&&this.colorTextures.forEach(e=>{e.destroy()}),this.depthStencilTexture&&(this.depthStencilTexture.destroy(),delete this.depthStencilTexture)}};ke.defaultOptions={width:0,height:0,resolution:1,colorTextures:1,stencil:!1,depth:!1,antialias:!1,isRoot:!1};let G=ke;class Lt{constructor(e){this.rootViewPort=new w,this.viewport=new w,this.onRenderTargetChange=new $e("onRenderTargetChange"),this.projectionMatrix=new v,this.defaultClearColor=[0,0,0,0],this._renderSurfaceToRenderTargetHash=new Map,this._gpuRenderTargetHash=Object.create(null),this._renderTargetStack=[],this._renderer=e,e.renderableGC.addManagedHash(this,"_gpuRenderTargetHash")}finishRenderPass(){this.adaptor.finishRenderPass(this.renderTarget)}renderStart({target:e,clear:t,clearColor:r,frame:s}){var a,i;this._renderTargetStack.length=0,this.push(e,t,r,s),this.rootViewPort.copyFrom(this.viewport),this.rootRenderTarget=this.renderTarget,this.renderingToScreen=ct(this.rootRenderTarget),(i=(a=this.adaptor).prerender)==null||i.call(a,this.rootRenderTarget)}postrender(){var e,t;(t=(e=this.adaptor).postrender)==null||t.call(e,this.rootRenderTarget)}bind(e,t=!0,r,s){const a=this.getRenderTarget(e),i=this.renderTarget!==a;this.renderTarget=a,this.renderSurface=e;const o=this.getGpuRenderTarget(a);(a.pixelWidth!==o.width||a.pixelHeight!==o.height)&&(this.adaptor.resizeGpuRenderTarget(a),o.width=a.pixelWidth,o.height=a.pixelHeight);const l=a.colorTexture,d=this.viewport,c=l.pixelWidth,h=l.pixelHeight;if(!s&&e instanceof m&&(s=e.frame),s){const p=l._resolution;d.x=s.x*p+.5|0,d.y=s.y*p+.5|0,d.width=s.width*p+.5|0,d.height=s.height*p+.5|0}else d.x=0,d.y=0,d.width=c,d.height=h;return dt(this.projectionMatrix,0,0,d.width/l.resolution,d.height/l.resolution,!a.isRoot),this.adaptor.startRenderPass(a,t,r,d),i&&this.onRenderTargetChange.emit(a),a}clear(e,t=B.ALL,r){t&&(e&&(e=this.getRenderTarget(e)),this.adaptor.clear(e||this.renderTarget,t,r,this.viewport))}contextChange(){this._gpuRenderTargetHash=Object.create(null)}push(e,t=B.ALL,r,s){const a=this.bind(e,t,r,s);return this._renderTargetStack.push({renderTarget:a,frame:s}),a}pop(){this._renderTargetStack.pop();const e=this._renderTargetStack[this._renderTargetStack.length-1];this.bind(e.renderTarget,!1,null,e.frame)}getRenderTarget(e){return e.isTexture&&(e=e.source),this._renderSurfaceToRenderTargetHash.get(e)??this._initRenderTarget(e)}copyToTexture(e,t,r,s,a){r.x<0&&(s.width+=r.x,a.x-=r.x,r.x=0),r.y<0&&(s.height+=r.y,a.y-=r.y,r.y=0);const{pixelWidth:i,pixelHeight:o}=e;return s.width=Math.min(s.width,i-r.x),s.height=Math.min(s.height,o-r.y),this.adaptor.copyToTexture(e,t,r,s,a)}ensureDepthStencil(){this.renderTarget.stencil||(this.renderTarget.stencil=!0,this.adaptor.startRenderPass(this.renderTarget,!1,null,this.viewport))}destroy(){this._renderer=null,this._renderSurfaceToRenderTargetHash.forEach((e,t)=>{e!==t&&e.destroy()}),this._renderSurfaceToRenderTargetHash.clear(),this._gpuRenderTargetHash=Object.create(null)}_initRenderTarget(e){let t=null;return U.test(e)&&(e=ye(e).source),e instanceof G?t=e:e instanceof T&&(t=new G({colorTextures:[e]}),e.source instanceof U&&(t.isRoot=!0),e.once("destroy",()=>{t.destroy(),this._renderSurfaceToRenderTargetHash.delete(e);const r=this._gpuRenderTargetHash[t.uid];r&&(this._gpuRenderTargetHash[t.uid]=null,this.adaptor.destroyGpuRenderTarget(r))})),this._renderSurfaceToRenderTargetHash.set(e,t),t}getGpuRenderTarget(e){return this._gpuRenderTargetHash[e.uid]||(this._gpuRenderTargetHash[e.uid]=this.adaptor.initGpuRenderTarget(e))}resetState(){this.renderTarget=null,this.renderSurface=null}}class Ht extends qe{constructor({buffer:e,offset:t,size:r}){super(),this.uid=M("buffer"),this._resourceType="bufferResource",this._touched=0,this._resourceId=M("resource"),this._bufferResource=!0,this.destroyed=!1,this.buffer=e,this.offset=t|0,this.size=r,this.buffer.on("change",this.onBufferChange,this)}onBufferChange(){this._resourceId=M("resource"),this.emit("change",this)}destroy(e=!1){this.destroyed=!0,e&&this.buffer.destroy(),this.emit("change",this),this.buffer=null}}class Me{constructor(e){this._renderer=e}updateRenderable(){}destroyRenderable(){}validateRenderable(){return!1}addRenderable(e,t){this._renderer.renderPipes.batch.break(t),t.add(e)}execute(e){e.isRenderable&&e.render(this._renderer)}destroy(){this._renderer=null}}Me.extension={type:[u.WebGLPipes,u.WebGPUPipes,u.CanvasPipes],name:"customRender"};function E(n,e){const t=n.instructionSet,r=t.instructions;for(let s=0;s1?1:t,n.worldAlpha=t,n.worldColorAlpha=n.worldColor+((t*255|0)<<24)}function Pe(n,e,t){if(e===n.updateTick)return;n.updateTick=e,n.didChange=!1;const r=n.localTransform;n.updateLocalTransform();const s=n.parent;if(s&&!s.renderGroup?(t|=n._updateFlags,n.relativeGroupTransform.appendFrom(r,s.relativeGroupTransform),t&re&&se(n,s,t)):(t=n._updateFlags,n.relativeGroupTransform.copyFrom(r),t&re&&se(n,ft,t)),!n.renderGroup){const a=n.children,i=a.length;for(let d=0;d1?1:r,n.groupAlpha=r,n.groupColorAlpha=n.groupColor+((r*255|0)<<24)}t&fe&&(n.groupBlendMode=n.localBlendMode==="inherit"?e.groupBlendMode:n.localBlendMode),t&ce&&(n.globalDisplayStatus=n.localDisplayStatus&e.globalDisplayStatus),n._updateFlags=0}function mt(n,e){const{list:t,index:r}=n.childrenRenderablesToUpdate;let s=!1;for(let a=0;a=0;r--)this._updateCachedRenderGroups(e.renderGroupChildren[r],t);if(e.invalidateMatrices(),e.isCachedAsTexture){if(e.textureNeedsUpdate){const r=e.root.getLocalBounds();r.ceil();const s=e.texture;e.texture&&b.returnTexture(e.texture,!0);const a=this._renderer,i=e.textureOptions.resolution||a.view.resolution,o=e.textureOptions.antialias??a.view.antialias,l=e.textureOptions.scaleMode??"linear",d=b.getOptimalTexture(r.width,r.height,i,o);d._source.style=new Ke({scaleMode:l}),e.texture=d,e._textureBounds||(e._textureBounds=new F),e._textureBounds.copyFrom(r),s!==e.texture&&e.renderGroupParent&&(e.renderGroupParent.structureDidChange=!0)}}else e.texture&&(b.returnTexture(e.texture,!0),e.texture=null)}_updateRenderGroups(e){const t=this._renderer,r=t.renderPipes;if(e.runOnRender(t),e.instructionSet.renderPipes=r,e.structureDidChange?D(e.childrenRenderablesToUpdate.list,0):mt(e,r),we(e),e.structureDidChange?(e.structureDidChange=!1,this._buildInstructions(e,t)):this._updateRenderables(e),e.childrenRenderablesToUpdate.index=0,t.renderPipes.batch.upload(e.instructionSet),!(e.isCachedAsTexture&&!e.textureNeedsUpdate))for(let s=0;s{if(!n.name)throw new Error("BlendMode extension must have a name property");y[n.name]=n.ref},n=>{delete y[n.name]});class Ae{constructor(e){this._blendModeStack=[],this._isAdvanced=!1,this._filterHash=Object.create(null),this._renderer=e,this._renderer.runners.prerender.add(this)}prerender(){this._activeBlendMode="normal",this._isAdvanced=!1}pushBlendMode(e,t,r){this._blendModeStack.push(t),this.setBlendMode(e,t,r)}popBlendMode(e){this._blendModeStack.pop();const t=this._blendModeStack[this._activeBlendMode.length-1]??"normal";this.setBlendMode(null,t,e)}setBlendMode(e,t,r){var a;const s=e instanceof Q;if(this._activeBlendMode===t){this._isAdvanced&&e&&!s&&((a=this._renderableList)==null||a.push(e));return}this._isAdvanced&&this._endAdvancedBlendMode(r),this._activeBlendMode=t,e&&(this._isAdvanced=!!y[t],this._isAdvanced&&this._beginAdvancedBlendMode(e,r))}_beginAdvancedBlendMode(e,t){this._renderer.renderPipes.batch.break(t);const r=this._activeBlendMode;if(!y[r]){H(`Unable to assign BlendMode: '${r}'. You may want to include: import 'pixi.js/advanced-blend-modes'`);return}const s=this._ensureFilterEffect(r),a=e instanceof Q,i={renderPipeId:"filter",action:"pushFilter",filterEffect:s,renderables:a?null:[e],container:a?e.root:null,canBundle:!1};this._renderableList=i.renderables,t.add(i)}_ensureFilterEffect(e){let t=this._filterHash[e];return t||(t=this._filterHash[e]=new ue,t.filters=[new y[e]]),t}_endAdvancedBlendMode(e){this._isAdvanced=!1,this._renderableList=null,this._renderer.renderPipes.batch.break(e),e.add({renderPipeId:"filter",action:"popFilter",canBundle:!1})}buildStart(){this._isAdvanced=!1}buildEnd(e){this._isAdvanced&&this._endAdvancedBlendMode(e)}destroy(){this._renderer=null,this._renderableList=null;for(const e in this._filterHash)this._filterHash[e].destroy();this._filterHash=null}}Ae.extension={type:[u.WebGLPipes,u.WebGPUPipes,u.CanvasPipes],name:"blendMode"};const R={png:"image/png",jpg:"image/jpeg",webp:"image/webp"},N=class Ie{constructor(e){this._renderer=e}_normalizeOptions(e,t={}){return e instanceof P||e instanceof m?{target:e,...t}:{...t,...e}}async image(e){const t=z.get().createImage();return t.src=await this.base64(e),t}async base64(e){e=this._normalizeOptions(e,Ie.defaultImageOptions);const{format:t,quality:r}=e,s=this.canvas(e);if(s.toBlob!==void 0)return new Promise((a,i)=>{s.toBlob(o=>{if(!o){i(new Error("ICanvas.toBlob failed!"));return}const l=new FileReader;l.onload=()=>a(l.result),l.onerror=i,l.readAsDataURL(o)},R[t],r)});if(s.toDataURL!==void 0)return s.toDataURL(R[t],r);if(s.convertToBlob!==void 0){const a=await s.convertToBlob({type:R[t],quality:r});return new Promise((i,o)=>{const l=new FileReader;l.onload=()=>i(l.result),l.onerror=o,l.readAsDataURL(a)})}throw new Error("Extract.base64() requires ICanvas.toDataURL, ICanvas.toBlob, or ICanvas.convertToBlob to be implemented")}canvas(e){e=this._normalizeOptions(e);const t=e.target,r=this._renderer;if(t instanceof m)return r.texture.generateCanvas(t);const s=r.textureGenerator.generateTexture(e),a=r.texture.generateCanvas(s);return s.destroy(!0),a}pixels(e){e=this._normalizeOptions(e);const t=e.target,r=this._renderer,s=t instanceof m?t:r.textureGenerator.generateTexture(e),a=r.texture.getPixels(s);return t instanceof P&&s.destroy(!0),a}texture(e){return e=this._normalizeOptions(e),e.target instanceof m?e.target:this._renderer.textureGenerator.generateTexture(e)}download(e){e=this._normalizeOptions(e);const t=this.canvas(e),r=document.createElement("a");r.download=e.filename??"image.png",r.href=t.toDataURL("image/png"),document.body.appendChild(r),r.click(),document.body.removeChild(r)}log(e){const t=e.width??200;e=this._normalizeOptions(e);const r=this.canvas(e),s=r.toDataURL();console.log(`[Pixi Texture] ${r.width}px ${r.height}px`);const a=["font-size: 1px;",`padding: ${t}px 300px;`,`background: url(${s}) no-repeat;`,"background-size: contain;"].join(" ");console.log("%c ",a)}destroy(){this._renderer=null}};N.extension={type:[u.WebGLSystem,u.WebGPUSystem],name:"extract"};N.defaultImageOptions={format:"png",quality:1};let xt=N;class j extends m{static create(e){return new j({source:new T(e)})}resize(e,t,r){return this.source.resize(e,t,r),this}}const _t=new w,bt=new F,Tt=[0,0,0,0];class Ge{constructor(e){this._renderer=e}generateTexture(e){var d;e instanceof P&&(e={target:e,frame:void 0,textureSourceOptions:{},resolution:void 0});const t=e.resolution||this._renderer.resolution,r=e.antialias||this._renderer.view.antialias,s=e.target;let a=e.clearColor;a?a=Array.isArray(a)&&a.length===4?a:A.shared.setValue(a).toArray():a=Tt;const i=((d=e.frame)==null?void 0:d.copyTo(_t))||Ye(s,bt).rectangle;i.width=Math.max(i.width,1/t)|0,i.height=Math.max(i.height,1/t)|0;const o=j.create({...e.textureSourceOptions,width:i.width,height:i.height,resolution:t,antialias:r}),l=v.shared.translate(-i.x,-i.y);return this._renderer.render({container:s,transform:l,target:o,clearColor:a}),o.source.updateMipmaps(),o}destroy(){this._renderer=null}}Ge.extension={type:[u.WebGLSystem,u.WebGPUSystem],name:"textureGenerator"};class Ee{constructor(e){this._stackIndex=0,this._globalUniformDataStack=[],this._uniformsPool=[],this._activeUniforms=[],this._bindGroupPool=[],this._activeBindGroups=[],this._renderer=e}reset(){this._stackIndex=0;for(let e=0;e"},uWorldTransformMatrix:{value:new v,type:"mat3x3"},uWorldColorAlpha:{value:new Float32Array(4),type:"vec4"},uResolution:{value:[0,0],type:"vec2"}},{isStatic:!0})}destroy(){this._renderer=null,this._globalUniformDataStack.length=0,this._uniformsPool.length=0,this._activeUniforms.length=0,this._bindGroupPool.length=0,this._activeBindGroups.length=0,this._currentGlobalUniformData=null}}Ee.extension={type:[u.WebGLSystem,u.WebGPUSystem,u.CanvasSystem],name:"globalUniforms"};let yt=1;class De{constructor(){this._tasks=[],this._offset=0}init(){Z.system.add(this._update,this)}repeat(e,t,r=!0){const s=yt++;let a=0;return r&&(this._offset+=1e3,a=this._offset),this._tasks.push({func:e,duration:t,start:performance.now(),offset:a,last:performance.now(),repeat:!0,id:s}),s}cancel(e){for(let t=0;t=r.duration){const s=e-r.start;r.func(s),r.last=e}}}destroy(){Z.system.remove(this._update,this),this._tasks.length=0}}De.extension={type:[u.WebGLSystem,u.WebGPUSystem,u.CanvasSystem],name:"scheduler",priority:0};let ae=!1;function kt(n){if(!ae){if(z.get().getNavigator().userAgent.toLowerCase().indexOf("chrome")>-1){const e=[`%c %c %c %c %c PixiJS %c v${ee} (${n}) http://www.pixijs.com/ + +`,"background: #E72264; padding:5px 0;","background: #6CA2EA; padding:5px 0;","background: #B5D33D; padding:5px 0;","background: #FED23F; padding:5px 0;","color: #FFFFFF; background: #E72264; padding:5px 0;","color: #E72264; background: #FFFFFF; padding:5px 0;"];globalThis.console.log(...e)}else globalThis.console&&globalThis.console.log(`PixiJS ${ee} - ${n} - http://www.pixijs.com/`);ae=!0}}class ${constructor(e){this._renderer=e}init(e){if(e.hello){let t=this._renderer.name;this._renderer.type===L.WEBGL&&(t+=` ${this._renderer.context.webGLVersion}`),kt(t)}}}$.extension={type:[u.WebGLSystem,u.WebGPUSystem,u.CanvasSystem],name:"hello",priority:-2};$.defaultOptions={hello:!1};function Ct(n){let e=!1;for(const r in n)if(n[r]==null){e=!0;break}if(!e)return n;const t=Object.create(null);for(const r in n){const s=n[r];s&&(t[r]=s)}return t}function Mt(n){let e=0;for(let t=0;tthis.run(),this._frequency,!1),this._hashHandler=this._renderer.scheduler.repeat(()=>{for(const t of this._managedHashes)t.context[t.hash]=Ct(t.context[t.hash])},this._frequency),this._arrayHandler=this._renderer.scheduler.repeat(()=>{for(const t of this._managedArrays)Mt(t.context[t.hash])},this._frequency)):(this._renderer.scheduler.cancel(this._handler),this._renderer.scheduler.cancel(this._hashHandler),this._renderer.scheduler.cancel(this._arrayHandler)))}addManagedHash(e,t){this._managedHashes.push({context:e,hash:t})}addManagedArray(e,t){this._managedArrays.push({context:e,hash:t})}prerender({container:e}){this._now=performance.now(),e.renderGroup.gcTick=St++,this._updateInstructionGCTick(e.renderGroup,e.renderGroup.gcTick)}addRenderable(e){this.enabled&&(e._lastUsed===-1&&(this._managedRenderables.push(e),e.once("destroyed",this._removeRenderable,this)),e._lastUsed=this._now)}run(){var a;const e=this._now,t=this._managedRenderables,r=this._renderer.renderPipes;let s=0;for(let i=0;ithis.maxUnusedTime){if(!o.destroyed){const c=r;l&&(l.structureDidChange=!0),c[o.renderPipeId].destroyRenderable(o)}o._lastUsed=-1,s++,o.off("destroyed",this._removeRenderable,this)}else t[i-s]=o}t.length-=s}destroy(){this.enabled=!1,this._renderer=null,this._managedRenderables.length=0,this._managedHashes.length=0,this._managedArrays.length=0}_removeRenderable(e){const t=this._managedRenderables.indexOf(e);t>=0&&(e.off("destroyed",this._removeRenderable,this),this._managedRenderables[t]=null)}_updateInstructionGCTick(e,t){e.instructionSet.gcTick=t;for(const r of e.renderGroupChildren)this._updateInstructionGCTick(r,t)}};q.extension={type:[u.WebGLSystem,u.WebGPUSystem],name:"renderableGC",priority:0};q.defaultOptions={renderableGCActive:!0,renderableGCMaxUnusedTime:6e4,renderableGCFrequency:3e4};let wt=q;const K=class Fe{constructor(e){this._renderer=e,this.count=0,this.checkCount=0}init(e){e={...Fe.defaultOptions,...e},this.checkCountMax=e.textureGCCheckCountMax,this.maxIdle=e.textureGCAMaxIdle??e.textureGCMaxIdle,this.active=e.textureGCActive}postrender(){this._renderer.renderingToScreen&&(this.count++,this.active&&(this.checkCount++,this.checkCount>this.checkCountMax&&(this.checkCount=0,this.run())))}run(){const e=this._renderer.texture.managedTextures;for(let t=0;t-1&&this.count-r._touched>this.maxIdle&&(r._touched=-1,r.unload())}}destroy(){this._renderer=null}};K.extension={type:[u.WebGLSystem,u.WebGPUSystem],name:"textureGC"};K.defaultOptions={textureGCActive:!0,textureGCAMaxIdle:null,textureGCMaxIdle:60*60,textureGCCheckCountMax:600};let Pt=K;const Y=class Le{get autoDensity(){return this.texture.source.autoDensity}set autoDensity(e){this.texture.source.autoDensity=e}get resolution(){return this.texture.source._resolution}set resolution(e){this.texture.source.resize(this.texture.source.width,this.texture.source.height,e)}init(e){e={...Le.defaultOptions,...e},e.view&&(Qe(Ze,"ViewSystem.view has been renamed to ViewSystem.canvas"),e.canvas=e.view),this.screen=new w(0,0,e.width,e.height),this.canvas=e.canvas||z.get().createCanvas(),this.antialias=!!e.antialias,this.texture=ye(this.canvas,e),this.renderTarget=new G({colorTextures:[this.texture],depth:!!e.depth,isRoot:!0}),this.texture.source.transparent=e.backgroundAlpha<1,this.resolution=e.resolution}resize(e,t,r){this.texture.source.resize(e,t,r),this.screen.width=this.texture.frame.width,this.screen.height=this.texture.frame.height}destroy(e=!1){(typeof e=="boolean"?e:!!(e!=null&&e.removeView))&&this.canvas.parentNode&&this.canvas.parentNode.removeChild(this.canvas),this.texture.destroy()}};Y.extension={type:[u.WebGLSystem,u.WebGPUSystem,u.CanvasSystem],name:"view",priority:0};Y.defaultOptions={width:800,height:600,autoDensity:!1,antialias:!1};let Rt=Y;const zt=[gt,Ee,$,Rt,Re,Pt,Ge,xt,et,wt,De],Wt=[Ae,xe,Be,Se,_e,Te,be,Me];export{Ht as B,_ as G,Lt as R,zt as S,Dt as U,Wt as a,ut as b,Ot as c,C as d,Et as e,Gt as f,It as t,Ft as u}; diff --git a/dist/assets/WebGLRenderer-BVO0qznQ.js b/dist/assets/WebGLRenderer-BVO0qznQ.js new file mode 100644 index 0000000..2c9172d --- /dev/null +++ b/dist/assets/WebGLRenderer-BVO0qznQ.js @@ -0,0 +1,156 @@ +import{E as d,B,w as m,D as S,J as K,t as Be,q as U,s as b,a5 as Ae,j as $,a6 as Ne,S as p,O as H,i as A,h as N,k as F,M as z,a7 as Y,a8 as ye,a9 as Ce,aa as q,ab as Ie,A as De,R as Ge,e as T}from"./index-DB6B0XuI.js";import{S as O,b as Z}from"./colorToUniform-DmtBy-2V.js";import{e as Ue,G as Fe,c as Oe,b as Pe,U as Me,R as Le,B as J,d as y,f as He,S as we,a as Ve}from"./SharedSystems-DVK37F7d.js";class Q{constructor(){this._tempState=O.for2d(),this._didUploadHash={}}init(e){e.renderer.runners.contextChange.add(this)}contextChange(){this._didUploadHash={}}start(e,r,s){const n=e.renderer,i=this._didUploadHash[s.uid];n.shader.bind(s,i),i||(this._didUploadHash[s.uid]=!0),n.shader.updateUniformGroup(n.globalUniforms.uniformGroup),n.geometry.bind(r,s.glProgram)}execute(e,r){const s=e.renderer;this._tempState.blendMode=r.blendMode,s.state.set(this._tempState);const n=r.textures.textures;for(let i=0;i(t[t.ELEMENT_ARRAY_BUFFER=34963]="ELEMENT_ARRAY_BUFFER",t[t.ARRAY_BUFFER=34962]="ARRAY_BUFFER",t[t.UNIFORM_BUFFER=35345]="UNIFORM_BUFFER",t))(x||{});class ke{constructor(e,r){this._lastBindBaseLocation=-1,this._lastBindCallId=-1,this.buffer=e||null,this.updateID=-1,this.byteLength=-1,this.type=r}}class ee{constructor(e){this._gpuBuffers=Object.create(null),this._boundBufferBases=Object.create(null),this._minBaseLocation=0,this._nextBindBaseIndex=this._minBaseLocation,this._bindCallId=0,this._renderer=e,this._renderer.renderableGC.addManagedHash(this,"_gpuBuffers")}destroy(){this._renderer=null,this._gl=null,this._gpuBuffers=null,this._boundBufferBases=null}contextChange(){this._gl=this._renderer.gl,this._gpuBuffers=Object.create(null),this._maxBindings=this._renderer.limits.maxUniformBindings}getGlBuffer(e){return this._gpuBuffers[e.uid]||this.createGLBuffer(e)}bind(e){const{_gl:r}=this,s=this.getGlBuffer(e);r.bindBuffer(s.type,s.buffer)}bindBufferBase(e,r){const{_gl:s}=this;this._boundBufferBases[r]!==e&&(this._boundBufferBases[r]=e,e._lastBindBaseLocation=r,s.bindBufferBase(s.UNIFORM_BUFFER,r,e.buffer))}nextBindBase(e){this._bindCallId++,this._minBaseLocation=0,e&&(this._boundBufferBases[0]=null,this._minBaseLocation=1,this._nextBindBaseIndex<1&&(this._nextBindBaseIndex=1))}freeLocationForBufferBase(e){let r=this.getLastBindBaseLocation(e);if(r>=this._minBaseLocation)return e._lastBindCallId=this._bindCallId,r;let s=0,n=this._nextBindBaseIndex;for(;s<2;){n>=this._maxBindings&&(n=this._minBaseLocation,s++);const i=this._boundBufferBases[n];if(i&&i._lastBindCallId===this._bindCallId){n++;continue}break}return r=n,this._nextBindBaseIndex=n+1,s>=2?-1:(e._lastBindCallId=this._bindCallId,this._boundBufferBases[r]=null,r)}getLastBindBaseLocation(e){const r=e._lastBindBaseLocation;return this._boundBufferBases[r]===e?r:-1}bindBufferRange(e,r,s,n){const{_gl:i}=this;s||(s=0),r||(r=0),this._boundBufferBases[r]=null,i.bindBufferRange(i.UNIFORM_BUFFER,r||0,e.buffer,s*256,n||256)}updateBuffer(e){const{_gl:r}=this,s=this.getGlBuffer(e);if(e._updateID===s.updateID)return s;s.updateID=e._updateID,r.bindBuffer(s.type,s.buffer);const n=e.data,i=e.descriptor.usage&B.STATIC?r.STATIC_DRAW:r.DYNAMIC_DRAW;return n?s.byteLength>=n.byteLength?r.bufferSubData(s.type,0,n,0,e._updateSize/n.BYTES_PER_ELEMENT):(s.byteLength=n.byteLength,r.bufferData(s.type,n,i)):(s.byteLength=e.descriptor.size,r.bufferData(s.type,s.byteLength,i)),s}destroyAll(){const e=this._gl;for(const r in this._gpuBuffers)e.deleteBuffer(this._gpuBuffers[r].buffer);this._gpuBuffers=Object.create(null)}onBufferDestroy(e,r){const s=this._gpuBuffers[e.uid],n=this._gl;r||n.deleteBuffer(s.buffer),this._gpuBuffers[e.uid]=null}createGLBuffer(e){const{_gl:r}=this;let s=x.ARRAY_BUFFER;e.descriptor.usage&B.INDEX?s=x.ELEMENT_ARRAY_BUFFER:e.descriptor.usage&B.UNIFORM&&(s=x.UNIFORM_BUFFER);const n=new ke(r.createBuffer(),s);return this._gpuBuffers[e.uid]=n,e.on("destroy",this.onBufferDestroy,this),n}resetState(){this._boundBufferBases=Object.create(null)}}ee.extension={type:[d.WebGLSystem],name:"buffer"};const P=class te{constructor(e){this.supports={uint32Indices:!0,uniformBufferObject:!0,vertexArrayObject:!0,srgbTextures:!0,nonPowOf2wrapping:!0,msaa:!0,nonPowOf2mipmaps:!0},this._renderer=e,this.extensions=Object.create(null),this.handleContextLost=this.handleContextLost.bind(this),this.handleContextRestored=this.handleContextRestored.bind(this)}get isLost(){return!this.gl||this.gl.isContextLost()}contextChange(e){this.gl=e,this._renderer.gl=e}init(e){e={...te.defaultOptions,...e};let r=this.multiView=e.multiView;if(e.context&&r&&(m("Renderer created with both a context and multiview enabled. Disabling multiView as both cannot work together."),r=!1),r?this.canvas=S.get().createCanvas(this._renderer.canvas.width,this._renderer.canvas.height):this.canvas=this._renderer.view.canvas,e.context)this.initFromContext(e.context);else{const s=this._renderer.background.alpha<1,n=e.premultipliedAlpha??!0,i=e.antialias&&!this._renderer.backBuffer.useBackBuffer;this.createContext(e.preferWebGLVersion,{alpha:s,premultipliedAlpha:n,antialias:i,stencil:!0,preserveDrawingBuffer:e.preserveDrawingBuffer,powerPreference:e.powerPreference??"default"})}}ensureCanvasSize(e){if(!this.multiView){e!==this.canvas&&m("multiView is disabled, but targetCanvas is not the main canvas");return}const{canvas:r}=this;(r.width{var r;this.gl.isContextLost()&&((r=this.extensions.loseContext)==null||r.restoreContext())},0))}handleContextRestored(){this.getExtensions(),this._renderer.runners.contextChange.emit(this.gl)}destroy(){var r;const e=this._renderer.view.canvas;this._renderer=null,e.removeEventListener("webglcontextlost",this.handleContextLost),e.removeEventListener("webglcontextrestored",this.handleContextRestored),this.gl.useProgram(null),(r=this.extensions.loseContext)==null||r.loseContext()}forceContextLoss(){var e;(e=this.extensions.loseContext)==null||e.loseContext(),this._contextLossForced=!0}validateContext(e){const r=e.getContextAttributes();r&&!r.stencil&&m("Provided WebGL context does not have a stencil buffer, masks may not render correctly");const s=this.supports,n=this.webGLVersion===2,i=this.extensions;s.uint32Indices=n||!!i.uint32ElementIndex,s.uniformBufferObject=n,s.vertexArrayObject=n||!!i.vertexArrayObject,s.srgbTextures=n||!!i.srgb,s.nonPowOf2wrapping=n,s.nonPowOf2mipmaps=n,s.msaa=n,s.uint32Indices||m("Provided WebGL context does not support 32 index buffer, large scenes may not render correctly")}};P.extension={type:[d.WebGLSystem],name:"context"};P.defaultOptions={context:null,premultipliedAlpha:!0,preserveDrawingBuffer:!1,powerPreference:void 0,preferWebGLVersion:2,multiView:!1};let Xe=P;var D=(t=>(t[t.RGBA=6408]="RGBA",t[t.RGB=6407]="RGB",t[t.RG=33319]="RG",t[t.RED=6403]="RED",t[t.RGBA_INTEGER=36249]="RGBA_INTEGER",t[t.RGB_INTEGER=36248]="RGB_INTEGER",t[t.RG_INTEGER=33320]="RG_INTEGER",t[t.RED_INTEGER=36244]="RED_INTEGER",t[t.ALPHA=6406]="ALPHA",t[t.LUMINANCE=6409]="LUMINANCE",t[t.LUMINANCE_ALPHA=6410]="LUMINANCE_ALPHA",t[t.DEPTH_COMPONENT=6402]="DEPTH_COMPONENT",t[t.DEPTH_STENCIL=34041]="DEPTH_STENCIL",t))(D||{}),re=(t=>(t[t.TEXTURE_2D=3553]="TEXTURE_2D",t[t.TEXTURE_CUBE_MAP=34067]="TEXTURE_CUBE_MAP",t[t.TEXTURE_2D_ARRAY=35866]="TEXTURE_2D_ARRAY",t[t.TEXTURE_CUBE_MAP_POSITIVE_X=34069]="TEXTURE_CUBE_MAP_POSITIVE_X",t[t.TEXTURE_CUBE_MAP_NEGATIVE_X=34070]="TEXTURE_CUBE_MAP_NEGATIVE_X",t[t.TEXTURE_CUBE_MAP_POSITIVE_Y=34071]="TEXTURE_CUBE_MAP_POSITIVE_Y",t[t.TEXTURE_CUBE_MAP_NEGATIVE_Y=34072]="TEXTURE_CUBE_MAP_NEGATIVE_Y",t[t.TEXTURE_CUBE_MAP_POSITIVE_Z=34073]="TEXTURE_CUBE_MAP_POSITIVE_Z",t[t.TEXTURE_CUBE_MAP_NEGATIVE_Z=34074]="TEXTURE_CUBE_MAP_NEGATIVE_Z",t))(re||{}),h=(t=>(t[t.UNSIGNED_BYTE=5121]="UNSIGNED_BYTE",t[t.UNSIGNED_SHORT=5123]="UNSIGNED_SHORT",t[t.UNSIGNED_SHORT_5_6_5=33635]="UNSIGNED_SHORT_5_6_5",t[t.UNSIGNED_SHORT_4_4_4_4=32819]="UNSIGNED_SHORT_4_4_4_4",t[t.UNSIGNED_SHORT_5_5_5_1=32820]="UNSIGNED_SHORT_5_5_5_1",t[t.UNSIGNED_INT=5125]="UNSIGNED_INT",t[t.UNSIGNED_INT_10F_11F_11F_REV=35899]="UNSIGNED_INT_10F_11F_11F_REV",t[t.UNSIGNED_INT_2_10_10_10_REV=33640]="UNSIGNED_INT_2_10_10_10_REV",t[t.UNSIGNED_INT_24_8=34042]="UNSIGNED_INT_24_8",t[t.UNSIGNED_INT_5_9_9_9_REV=35902]="UNSIGNED_INT_5_9_9_9_REV",t[t.BYTE=5120]="BYTE",t[t.SHORT=5122]="SHORT",t[t.INT=5124]="INT",t[t.FLOAT=5126]="FLOAT",t[t.FLOAT_32_UNSIGNED_INT_24_8_REV=36269]="FLOAT_32_UNSIGNED_INT_24_8_REV",t[t.HALF_FLOAT=36193]="HALF_FLOAT",t))(h||{});const w={uint8x2:h.UNSIGNED_BYTE,uint8x4:h.UNSIGNED_BYTE,sint8x2:h.BYTE,sint8x4:h.BYTE,unorm8x2:h.UNSIGNED_BYTE,unorm8x4:h.UNSIGNED_BYTE,snorm8x2:h.BYTE,snorm8x4:h.BYTE,uint16x2:h.UNSIGNED_SHORT,uint16x4:h.UNSIGNED_SHORT,sint16x2:h.SHORT,sint16x4:h.SHORT,unorm16x2:h.UNSIGNED_SHORT,unorm16x4:h.UNSIGNED_SHORT,snorm16x2:h.SHORT,snorm16x4:h.SHORT,float16x2:h.HALF_FLOAT,float16x4:h.HALF_FLOAT,float32:h.FLOAT,float32x2:h.FLOAT,float32x3:h.FLOAT,float32x4:h.FLOAT,uint32:h.UNSIGNED_INT,uint32x2:h.UNSIGNED_INT,uint32x3:h.UNSIGNED_INT,uint32x4:h.UNSIGNED_INT,sint32:h.INT,sint32x2:h.INT,sint32x3:h.INT,sint32x4:h.INT};function We(t){return w[t]??w.float32}const je={"point-list":0,"line-list":1,"line-strip":3,"triangle-list":4,"triangle-strip":5};class se{constructor(e){this._geometryVaoHash=Object.create(null),this._renderer=e,this._activeGeometry=null,this._activeVao=null,this.hasVao=!0,this.hasInstance=!0,this._renderer.renderableGC.addManagedHash(this,"_geometryVaoHash")}contextChange(){const e=this.gl=this._renderer.gl;if(!this._renderer.context.supports.vertexArrayObject)throw new Error("[PixiJS] Vertex Array Objects are not supported on this device");const r=this._renderer.context.extensions.vertexArrayObject;r&&(e.createVertexArray=()=>r.createVertexArrayOES(),e.bindVertexArray=n=>r.bindVertexArrayOES(n),e.deleteVertexArray=n=>r.deleteVertexArrayOES(n));const s=this._renderer.context.extensions.vertexAttribDivisorANGLE;s&&(e.drawArraysInstanced=(n,i,a,o)=>{s.drawArraysInstancedANGLE(n,i,a,o)},e.drawElementsInstanced=(n,i,a,o,c)=>{s.drawElementsInstancedANGLE(n,i,a,o,c)},e.vertexAttribDivisor=(n,i)=>s.vertexAttribDivisorANGLE(n,i)),this._activeGeometry=null,this._activeVao=null,this._geometryVaoHash=Object.create(null)}bind(e,r){const s=this.gl;this._activeGeometry=e;const n=this.getVao(e,r);this._activeVao!==n&&(this._activeVao=n,s.bindVertexArray(n)),this.updateBuffers()}resetState(){this.unbind()}updateBuffers(){const e=this._activeGeometry,r=this._renderer.buffer;for(let s=0;s1?i.drawElementsInstanced(o,r||a.indexBuffer.data.length,u,(s||0)*c,n):i.drawElements(o,r||a.indexBuffer.data.length,u,(s||0)*c)}else n>1?i.drawArraysInstanced(o,s||0,r||a.getSize(),n):i.drawArrays(o,s||0,r||a.getSize());return this}unbind(){this.gl.bindVertexArray(null),this._activeVao=null,this._activeGeometry=null}destroy(){this._renderer=null,this.gl=null,this._activeVao=null,this._activeGeometry=null,this._geometryVaoHash=null}}se.extension={type:[d.WebGLSystem],name:"geometry"};const Ke=new Ae({attributes:{aPosition:[-1,-1,3,-1,-1,3]}}),M=class ne{constructor(e){this.useBackBuffer=!1,this._useBackBufferThisRender=!1,this._renderer=e}init(e={}){const{useBackBuffer:r,antialias:s}={...ne.defaultOptions,...e};this.useBackBuffer=r,this._antialias=s,this._renderer.context.supports.msaa||(m("antialiasing, is not supported on when using the back buffer"),this._antialias=!1),this._state=O.for2d();const n=new Be({vertex:` + attribute vec2 aPosition; + out vec2 vUv; + + void main() { + gl_Position = vec4(aPosition, 0.0, 1.0); + + vUv = (aPosition + 1.0) / 2.0; + + // flip dem UVs + vUv.y = 1.0 - vUv.y; + }`,fragment:` + in vec2 vUv; + out vec4 finalColor; + + uniform sampler2D uTexture; + + void main() { + finalColor = texture(uTexture, vUv); + }`,name:"big-triangle"});this._bigTriangleShader=new U({glProgram:n,resources:{uTexture:b.WHITE.source}})}renderStart(e){const r=this._renderer.renderTarget.getRenderTarget(e.target);if(this._useBackBufferThisRender=this.useBackBuffer&&!!r.isRoot,this._useBackBufferThisRender){const s=this._renderer.renderTarget.getRenderTarget(e.target);this._targetTexture=s.colorTexture,e.target=this._getBackBufferTexture(s.colorTexture)}}renderEnd(){this._presentBackBuffer()}_presentBackBuffer(){const e=this._renderer;e.renderTarget.finishRenderPass(),this._useBackBufferThisRender&&(e.renderTarget.bind(this._targetTexture,!1),this._bigTriangleShader.resources.uTexture=this._backBufferTexture.source,e.encoder.draw({geometry:Ke,shader:this._bigTriangleShader,state:this._state}))}_getBackBufferTexture(e){return this._backBufferTexture=this._backBufferTexture||new b({source:new $({width:e.width,height:e.height,resolution:e._resolution,antialias:this._antialias})}),this._backBufferTexture.source.resize(e.width,e.height,e._resolution),this._backBufferTexture}destroy(){this._backBufferTexture&&(this._backBufferTexture.destroy(),this._backBufferTexture=null)}};M.extension={type:[d.WebGLSystem],name:"backBuffer",priority:1};M.defaultOptions={useBackBuffer:!1};let $e=M;class ie{constructor(e){this._colorMaskCache=15,this._renderer=e}setMask(e){this._colorMaskCache!==e&&(this._colorMaskCache=e,this._renderer.gl.colorMask(!!(e&8),!!(e&4),!!(e&2),!!(e&1)))}}ie.extension={type:[d.WebGLSystem],name:"colorMask"};class ae{constructor(e){this.commandFinished=Promise.resolve(),this._renderer=e}setGeometry(e,r){this._renderer.geometry.bind(e,r.glProgram)}finishRenderPass(){}draw(e){const r=this._renderer,{geometry:s,shader:n,state:i,skipSync:a,topology:o,size:c,start:u,instanceCount:_}=e;r.shader.bind(n,a),r.geometry.bind(s,r.shader._activeProgram),i&&r.state.set(i),r.geometry.draw(o,c,u,_??s.instanceCount)}destroy(){this._renderer=null}}ae.extension={type:[d.WebGLSystem],name:"encoder"};class oe{constructor(e){this._renderer=e}contextChange(){const e=this._renderer.gl;this.maxTextures=e.getParameter(e.MAX_TEXTURE_IMAGE_UNITS),this.maxBatchableTextures=Ne(this.maxTextures,e);const r=this._renderer.context.webGLVersion===2;this.maxUniformBindings=r?e.getParameter(e.MAX_UNIFORM_BUFFER_BINDINGS):0}destroy(){}}oe.extension={type:[d.WebGLSystem],name:"limits"};class ze{constructor(){this.width=-1,this.height=-1,this.msaa=!1,this.msaaRenderBuffer=[]}}class ce{constructor(e){this._stencilCache={enabled:!1,stencilReference:0,stencilMode:p.NONE},this._renderTargetStencilState=Object.create(null),e.renderTarget.onRenderTargetChange.add(this)}contextChange(e){this._gl=e,this._comparisonFuncMapping={always:e.ALWAYS,never:e.NEVER,equal:e.EQUAL,"not-equal":e.NOTEQUAL,less:e.LESS,"less-equal":e.LEQUAL,greater:e.GREATER,"greater-equal":e.GEQUAL},this._stencilOpsMapping={keep:e.KEEP,zero:e.ZERO,replace:e.REPLACE,invert:e.INVERT,"increment-clamp":e.INCR,"decrement-clamp":e.DECR,"increment-wrap":e.INCR_WRAP,"decrement-wrap":e.DECR_WRAP},this.resetState()}onRenderTargetChange(e){if(this._activeRenderTarget===e)return;this._activeRenderTarget=e;let r=this._renderTargetStencilState[e.uid];r||(r=this._renderTargetStencilState[e.uid]={stencilMode:p.DISABLED,stencilReference:0}),this.setStencilMode(r.stencilMode,r.stencilReference)}resetState(){this._stencilCache.enabled=!1,this._stencilCache.stencilMode=p.NONE,this._stencilCache.stencilReference=0}setStencilMode(e,r){const s=this._renderTargetStencilState[this._activeRenderTarget.uid],n=this._gl,i=Fe[e],a=this._stencilCache;if(s.stencilMode=e,s.stencilReference=r,e===p.DISABLED){this._stencilCache.enabled&&(this._stencilCache.enabled=!1,n.disable(n.STENCIL_TEST));return}this._stencilCache.enabled||(this._stencilCache.enabled=!0,n.enable(n.STENCIL_TEST)),(e!==a.stencilMode||a.stencilReference!==r)&&(a.stencilMode=e,a.stencilReference=r,n.stencilFunc(this._comparisonFuncMapping[i.stencilBack.compare],r,255),n.stencilOp(n.KEEP,n.KEEP,this._stencilOpsMapping[i.stencilBack.passOp]))}}ce.extension={type:[d.WebGLSystem],name:"stencil"};const ue={f32:4,i32:4,"vec2":8,"vec3":12,"vec4":16,"vec2":8,"vec3":12,"vec4":16,"mat2x2":16*2,"mat3x3":16*3,"mat4x4":16*4};function Ye(t){const e=t.map(i=>({data:i,offset:0,size:0})),r=16;let s=0,n=0;for(let i=0;i1&&(s=Math.max(s,r)*a.data.size);const o=s===12?16:s;a.size=s;const c=n%r;c>0&&r-c=0?"dataInt32":"data";return` + v = uv.${t.data.name}; + offset += ${e}; + + arrayOffset = offset; + + t = 0; + + for(var i=0; i < ${t.data.size*r}; i++) + { + for(var j = 0; j < ${s}; j++) + { + ${i}[arrayOffset++] = v[t++]; + } + ${n!==0?`arrayOffset += ${n};`:""} + } + `}function Ze(t){return Oe(t,"uboStd40",qe,Pe)}class _e extends Me{constructor(){super({createUboElements:Ye,generateUboSync:Ze})}}_e.extension={type:[d.WebGLSystem],name:"ubo"};class Je{constructor(){this._clearColorCache=[0,0,0,0],this._viewPortCache=new H}init(e,r){this._renderer=e,this._renderTargetSystem=r,e.runners.contextChange.add(this)}contextChange(){this._clearColorCache=[0,0,0,0],this._viewPortCache=new H}copyToTexture(e,r,s,n,i){const a=this._renderTargetSystem,o=this._renderer,c=a.getGpuRenderTarget(e),u=o.gl;return this.finishRenderPass(e),u.bindFramebuffer(u.FRAMEBUFFER,c.resolveTargetFramebuffer),o.texture.bind(r,0),u.copyTexSubImage2D(u.TEXTURE_2D,0,i.x,i.y,s.x,s.y,n.width,n.height),r}startRenderPass(e,r=!0,s,n){const i=this._renderTargetSystem,a=e.colorTexture,o=i.getGpuRenderTarget(e);let c=n.y;e.isRoot&&(c=a.pixelHeight-n.height),e.colorTextures.forEach(f=>{this._renderer.texture.unbind(f)});const u=this._renderer.gl;u.bindFramebuffer(u.FRAMEBUFFER,o.framebuffer);const _=this._viewPortCache;(_.x!==n.x||_.y!==c||_.width!==n.width||_.height!==n.height)&&(_.x=n.x,_.y=c,_.width=n.width,_.height=n.height,u.viewport(n.x,c,n.width,n.height)),!o.depthStencilRenderBuffer&&(e.stencil||e.depth)&&this._initStencil(o),this.clear(e,r,s)}finishRenderPass(e){const s=this._renderTargetSystem.getGpuRenderTarget(e);if(!s.msaa)return;const n=this._renderer.gl;n.bindFramebuffer(n.FRAMEBUFFER,s.resolveTargetFramebuffer),n.bindFramebuffer(n.READ_FRAMEBUFFER,s.framebuffer),n.blitFramebuffer(0,0,s.width,s.height,0,0,s.width,s.height,n.COLOR_BUFFER_BIT,n.NEAREST),n.bindFramebuffer(n.FRAMEBUFFER,s.framebuffer)}initGpuRenderTarget(e){const s=this._renderer.gl,n=new ze;return e.colorTexture instanceof A?(this._renderer.context.ensureCanvasSize(e.colorTexture.resource),n.framebuffer=null,n):(this._initColor(e,n),s.bindFramebuffer(s.FRAMEBUFFER,null),n)}destroyGpuRenderTarget(e){const r=this._renderer.gl;e.framebuffer&&(r.deleteFramebuffer(e.framebuffer),e.framebuffer=null),e.resolveTargetFramebuffer&&(r.deleteFramebuffer(e.resolveTargetFramebuffer),e.resolveTargetFramebuffer=null),e.depthStencilRenderBuffer&&(r.deleteRenderbuffer(e.depthStencilRenderBuffer),e.depthStencilRenderBuffer=null),e.msaaRenderBuffer.forEach(s=>{r.deleteRenderbuffer(s)}),e.msaaRenderBuffer=null}clear(e,r,s){if(!r)return;const n=this._renderTargetSystem;typeof r=="boolean"&&(r=r?N.ALL:N.NONE);const i=this._renderer.gl;if(r&N.COLOR){s??(s=n.defaultClearColor);const a=this._clearColorCache,o=s;(a[0]!==o[0]||a[1]!==o[1]||a[2]!==o[2]||a[3]!==o[3])&&(a[0]=o[0],a[1]=o[1],a[2]=o[2],a[3]=o[3],i.clearColor(o[0],o[1],o[2],o[3]))}i.clear(r)}resizeGpuRenderTarget(e){if(e.isRoot)return;const s=this._renderTargetSystem.getGpuRenderTarget(e);this._resizeColor(e,s),(e.stencil||e.depth)&&this._resizeStencil(s)}_initColor(e,r){const s=this._renderer,n=s.gl,i=n.createFramebuffer();if(r.resolveTargetFramebuffer=i,n.bindFramebuffer(n.FRAMEBUFFER,i),r.width=e.colorTexture.source.pixelWidth,r.height=e.colorTexture.source.pixelHeight,e.colorTextures.forEach((a,o)=>{const c=a.source;c.antialias&&(s.context.supports.msaa?r.msaa=!0:m("[RenderTexture] Antialiasing on textures is not supported in WebGL1")),s.texture.bindSource(c,0);const _=s.texture.getGlSource(c).texture;n.framebufferTexture2D(n.FRAMEBUFFER,n.COLOR_ATTACHMENT0+o,3553,_,0)}),r.msaa){const a=n.createFramebuffer();r.framebuffer=a,n.bindFramebuffer(n.FRAMEBUFFER,a),e.colorTextures.forEach((o,c)=>{const u=n.createRenderbuffer();r.msaaRenderBuffer[c]=u})}else r.framebuffer=i;this._resizeColor(e,r)}_resizeColor(e,r){const s=e.colorTexture.source;if(r.width=s.pixelWidth,r.height=s.pixelHeight,e.colorTextures.forEach((n,i)=>{i!==0&&n.source.resize(s.width,s.height,s._resolution)}),r.msaa){const n=this._renderer,i=n.gl,a=r.framebuffer;i.bindFramebuffer(i.FRAMEBUFFER,a),e.colorTextures.forEach((o,c)=>{const u=o.source;n.texture.bindSource(u,0);const f=n.texture.getGlSource(u).internalFormat,l=r.msaaRenderBuffer[c];i.bindRenderbuffer(i.RENDERBUFFER,l),i.renderbufferStorageMultisample(i.RENDERBUFFER,4,f,u.pixelWidth,u.pixelHeight),i.framebufferRenderbuffer(i.FRAMEBUFFER,i.COLOR_ATTACHMENT0+c,i.RENDERBUFFER,l)})}}_initStencil(e){if(e.framebuffer===null)return;const r=this._renderer.gl,s=r.createRenderbuffer();e.depthStencilRenderBuffer=s,r.bindRenderbuffer(r.RENDERBUFFER,s),r.framebufferRenderbuffer(r.FRAMEBUFFER,r.DEPTH_STENCIL_ATTACHMENT,r.RENDERBUFFER,s),this._resizeStencil(e)}_resizeStencil(e){const r=this._renderer.gl;r.bindRenderbuffer(r.RENDERBUFFER,e.depthStencilRenderBuffer),e.msaa?r.renderbufferStorageMultisample(r.RENDERBUFFER,4,r.DEPTH24_STENCIL8,e.width,e.height):r.renderbufferStorage(r.RENDERBUFFER,this._renderer.context.webGLVersion===2?r.DEPTH24_STENCIL8:r.DEPTH_STENCIL,e.width,e.height)}prerender(e){const r=e.colorTexture.resource;this._renderer.context.multiView&&A.test(r)&&this._renderer.context.ensureCanvasSize(r)}postrender(e){if(this._renderer.context.multiView&&A.test(e.colorTexture.resource)){const r=this._renderer.context.canvas,s=e.colorTexture;s.context2D.drawImage(r,0,s.pixelHeight-r.height)}}}class he extends Le{constructor(e){super(e),this.adaptor=new Je,this.adaptor.init(e,this)}}he.extension={type:[d.WebGLSystem],name:"renderTarget"};function Qe(t,e){const r=[],s=[` + var g = s.groups; + var sS = r.shader; + var p = s.glProgram; + var ugS = r.uniformGroup; + var resources; + `];let n=!1,i=0;const a=e._getProgramData(t.glProgram);for(const c in t.groups){const u=t.groups[c];r.push(` + resources = g[${c}].resources; + `);for(const _ in u.resources){const f=u.resources[_];if(f instanceof F)if(f.ubo){const l=t._uniformBindMap[c][Number(_)];r.push(` + sS.bindUniformBlock( + resources[${_}], + '${l}', + ${t.glProgram._uniformBlockData[l].index} + ); + `)}else r.push(` + ugS.updateUniformGroup(resources[${_}], p, sD); + `);else if(f instanceof J){const l=t._uniformBindMap[c][Number(_)];r.push(` + sS.bindUniformBlock( + resources[${_}], + '${l}', + ${t.glProgram._uniformBlockData[l].index} + ); + `)}else if(f instanceof $){const l=t._uniformBindMap[c][_],E=a.uniformData[l];E&&(n||(n=!0,s.push(` + var tS = r.texture; + `)),e._gl.uniform1i(E.location,i),r.push(` + tS.bind(resources[${_}], ${i}); + `),i++)}}}const o=[...s,...r].join(` +`);return new Function("r","s","sD",o)}class et{constructor(e,r){this.program=e,this.uniformData=r,this.uniformGroups={},this.uniformDirtyGroups={},this.uniformBlockBindings={}}destroy(){this.uniformData=null,this.uniformGroups=null,this.uniformDirtyGroups=null,this.uniformBlockBindings=null,this.program=null}}function V(t,e,r){const s=t.createShader(e);return t.shaderSource(s,r),t.compileShader(s),s}function C(t){const e=new Array(t);for(let r=0;ra>o?1:-1);for(let a=0;a`${_}: ${u}`),s=t.getShaderInfoLog(e),n=s.split(` +`),i={},a=n.map(u=>parseFloat(u.replace(/^ERROR\: 0\:([\d]+)\:.*$/,"$1"))).filter(u=>u&&!i[u]?(i[u]=!0,!0):!1),o=[""];a.forEach(u=>{r[u-1]=`%c${r[u-1]}%c`,o.push("background: #FF0000; color:#FFFFFF; font-size: 10px","font-size: 10px")});const c=r.join(` +`);o[0]=c,console.error(s),console.groupCollapsed("click to view full shader code"),console.warn(...o),console.groupEnd()}function at(t,e,r,s){t.getProgramParameter(e,t.LINK_STATUS)||(t.getShaderParameter(r,t.COMPILE_STATUS)||X(t,r),t.getShaderParameter(s,t.COMPILE_STATUS)||X(t,s),console.error("PixiJS Error: Could not initialize shader."),t.getProgramInfoLog(e)!==""&&console.warn("PixiJS Warning: gl.getProgramInfoLog()",t.getProgramInfoLog(e)))}function ot(t,e){const r=V(t,t.VERTEX_SHADER,e.vertex),s=V(t,t.FRAGMENT_SHADER,e.fragment),n=t.createProgram();t.attachShader(n,r),t.attachShader(n,s);const i=e.transformFeedbackVaryings;i&&(typeof t.transformFeedbackVaryings!="function"?m("TransformFeedback is not supported but TransformFeedbackVaryings are given."):t.transformFeedbackVaryings(n,i.names,i.bufferMode==="separate"?t.SEPARATE_ATTRIBS:t.INTERLEAVED_ATTRIBS)),t.linkProgram(n),t.getProgramParameter(n,t.LINK_STATUS)||at(t,n,r,s),e._attributeData=st(n,t,!/^[ \t]*#[ \t]*version[ \t]+300[ \t]+es[ \t]*$/m.test(e.vertex)),e._uniformData=it(n,t),e._uniformBlockData=nt(n,t),t.deleteShader(r),t.deleteShader(s);const a={};for(const c in e._uniformData){const u=e._uniformData[c];a[c]={location:t.getUniformLocation(n,c),value:fe(u.type,u.size)}}return new et(n,a)}const R={textureCount:0,blockIndex:0};class de{constructor(e){this._activeProgram=null,this._programDataHash=Object.create(null),this._shaderSyncFunctions=Object.create(null),this._renderer=e,this._renderer.renderableGC.addManagedHash(this,"_programDataHash")}contextChange(e){this._gl=e,this._programDataHash=Object.create(null),this._shaderSyncFunctions=Object.create(null),this._activeProgram=null}bind(e,r){if(this._setProgram(e.glProgram),r)return;R.textureCount=0,R.blockIndex=0;let s=this._shaderSyncFunctions[e.glProgram._key];s||(s=this._shaderSyncFunctions[e.glProgram._key]=this._generateShaderSync(e,this)),this._renderer.buffer.nextBindBase(!!e.glProgram.transformFeedbackVaryings),s(this._renderer,e,R)}updateUniformGroup(e){this._renderer.uniformGroup.updateUniformGroup(e,this._activeProgram,R)}bindUniformBlock(e,r,s=0){const n=this._renderer.buffer,i=this._getProgramData(this._activeProgram),a=e._bufferResource;a||this._renderer.ubo.updateUniformGroup(e);const o=e.buffer,c=n.updateBuffer(o),u=n.freeLocationForBufferBase(c);if(a){const{offset:f,size:l}=e;f===0&&l===o.data.byteLength?n.bindBufferBase(c,u):n.bindBufferRange(c,u,f)}else n.getLastBindBaseLocation(c)!==u&&n.bindBufferBase(c,u);const _=this._activeProgram._uniformBlockData[r].index;i.uniformBlockBindings[s]!==u&&(i.uniformBlockBindings[s]=u,this._renderer.gl.uniformBlockBinding(i.program,_,u))}_setProgram(e){if(this._activeProgram===e)return;this._activeProgram=e;const r=this._getProgramData(e);this._gl.useProgram(r.program)}_getProgramData(e){return this._programDataHash[e._key]||this._createProgramData(e)}_createProgramData(e){const r=e._key;return this._programDataHash[r]=ot(this._gl,e),this._programDataHash[r]}destroy(){for(const e of Object.keys(this._programDataHash))this._programDataHash[e].destroy(),this._programDataHash[e]=null;this._programDataHash=null,this._shaderSyncFunctions=null,this._activeProgram=null,this._renderer=null,this._gl=null}_generateShaderSync(e,r){return Qe(e,r)}resetState(){this._activeProgram=null}}de.extension={type:[d.WebGLSystem],name:"shader"};const ct={f32:`if (cv !== v) { + cu.value = v; + gl.uniform1f(location, v); + }`,"vec2":`if (cv[0] !== v[0] || cv[1] !== v[1]) { + cv[0] = v[0]; + cv[1] = v[1]; + gl.uniform2f(location, v[0], v[1]); + }`,"vec3":`if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2]) { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + gl.uniform3f(location, v[0], v[1], v[2]); + }`,"vec4":`if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2] || cv[3] !== v[3]) { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + cv[3] = v[3]; + gl.uniform4f(location, v[0], v[1], v[2], v[3]); + }`,i32:`if (cv !== v) { + cu.value = v; + gl.uniform1i(location, v); + }`,"vec2":`if (cv[0] !== v[0] || cv[1] !== v[1]) { + cv[0] = v[0]; + cv[1] = v[1]; + gl.uniform2i(location, v[0], v[1]); + }`,"vec3":`if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2]) { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + gl.uniform3i(location, v[0], v[1], v[2]); + }`,"vec4":`if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2] || cv[3] !== v[3]) { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + cv[3] = v[3]; + gl.uniform4i(location, v[0], v[1], v[2], v[3]); + }`,u32:`if (cv !== v) { + cu.value = v; + gl.uniform1ui(location, v); + }`,"vec2":`if (cv[0] !== v[0] || cv[1] !== v[1]) { + cv[0] = v[0]; + cv[1] = v[1]; + gl.uniform2ui(location, v[0], v[1]); + }`,"vec3":`if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2]) { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + gl.uniform3ui(location, v[0], v[1], v[2]); + }`,"vec4":`if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2] || cv[3] !== v[3]) { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + cv[3] = v[3]; + gl.uniform4ui(location, v[0], v[1], v[2], v[3]); + }`,bool:`if (cv !== v) { + cu.value = v; + gl.uniform1i(location, v); + }`,"vec2":`if (cv[0] !== v[0] || cv[1] !== v[1]) { + cv[0] = v[0]; + cv[1] = v[1]; + gl.uniform2i(location, v[0], v[1]); + }`,"vec3":`if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2]) { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + gl.uniform3i(location, v[0], v[1], v[2]); + }`,"vec4":`if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2] || cv[3] !== v[3]) { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + cv[3] = v[3]; + gl.uniform4i(location, v[0], v[1], v[2], v[3]); + }`,"mat2x2":"gl.uniformMatrix2fv(location, false, v);","mat3x3":"gl.uniformMatrix3fv(location, false, v);","mat4x4":"gl.uniformMatrix4fv(location, false, v);"},ut={f32:"gl.uniform1fv(location, v);","vec2":"gl.uniform2fv(location, v);","vec3":"gl.uniform3fv(location, v);","vec4":"gl.uniform4fv(location, v);","mat2x2":"gl.uniformMatrix2fv(location, false, v);","mat3x3":"gl.uniformMatrix3fv(location, false, v);","mat4x4":"gl.uniformMatrix4fv(location, false, v);",i32:"gl.uniform1iv(location, v);","vec2":"gl.uniform2iv(location, v);","vec3":"gl.uniform3iv(location, v);","vec4":"gl.uniform4iv(location, v);",u32:"gl.uniform1iv(location, v);","vec2":"gl.uniform2iv(location, v);","vec3":"gl.uniform3iv(location, v);","vec4":"gl.uniform4iv(location, v);",bool:"gl.uniform1iv(location, v);","vec2":"gl.uniform2iv(location, v);","vec3":"gl.uniform3iv(location, v);","vec4":"gl.uniform4iv(location, v);"};function _t(t,e){const r=[` + var v = null; + var cv = null; + var cu = null; + var t = 0; + var gl = renderer.gl; + var name = null; + `];for(const s in t.uniforms){if(!e[s]){t.uniforms[s]instanceof F?t.uniforms[s].ubo?r.push(` + renderer.shader.bindUniformBlock(uv.${s}, "${s}"); + `):r.push(` + renderer.shader.updateUniformGroup(uv.${s}); + `):t.uniforms[s]instanceof J&&r.push(` + renderer.shader.bindBufferResource(uv.${s}, "${s}"); + `);continue}const n=t.uniformStructures[s];let i=!1;for(let a=0;a>=1,s++;this.stateId=e.data}for(let r=0;r>1,1),n=Math.max(n>>1,1)}}},be={id:"image",upload(t,e,r,s){const n=e.width,i=e.height,a=t.pixelWidth,o=t.pixelHeight,c=t.resourceWidth,u=t.resourceHeight;c1){const u=Math.min(t.maxAnisotropy,e.getParameter(s.MAX_TEXTURE_MAX_ANISOTROPY_EXT));e[n](c,s.TEXTURE_MAX_ANISOTROPY_EXT,u)}t.compare&&e[n](c,e.TEXTURE_COMPARE_FUNC,Bt[t.compare])}function At(t){return{r8unorm:t.RED,r8snorm:t.RED,r8uint:t.RED,r8sint:t.RED,r16uint:t.RED,r16sint:t.RED,r16float:t.RED,rg8unorm:t.RG,rg8snorm:t.RG,rg8uint:t.RG,rg8sint:t.RG,r32uint:t.RED,r32sint:t.RED,r32float:t.RED,rg16uint:t.RG,rg16sint:t.RG,rg16float:t.RG,rgba8unorm:t.RGBA,"rgba8unorm-srgb":t.RGBA,rgba8snorm:t.RGBA,rgba8uint:t.RGBA,rgba8sint:t.RGBA,bgra8unorm:t.RGBA,"bgra8unorm-srgb":t.RGBA,rgb9e5ufloat:t.RGB,rgb10a2unorm:t.RGBA,rg11b10ufloat:t.RGB,rg32uint:t.RG,rg32sint:t.RG,rg32float:t.RG,rgba16uint:t.RGBA,rgba16sint:t.RGBA,rgba16float:t.RGBA,rgba32uint:t.RGBA,rgba32sint:t.RGBA,rgba32float:t.RGBA,stencil8:t.STENCIL_INDEX8,depth16unorm:t.DEPTH_COMPONENT,depth24plus:t.DEPTH_COMPONENT,"depth24plus-stencil8":t.DEPTH_STENCIL,depth32float:t.DEPTH_COMPONENT,"depth32float-stencil8":t.DEPTH_STENCIL}}function Nt(t,e){let r={},s=t.RGBA;return t instanceof S.get().getWebGLRenderingContext()?e.srgb&&(r={"rgba8unorm-srgb":e.srgb.SRGB8_ALPHA8_EXT,"bgra8unorm-srgb":e.srgb.SRGB8_ALPHA8_EXT}):(r={"rgba8unorm-srgb":t.SRGB8_ALPHA8,"bgra8unorm-srgb":t.SRGB8_ALPHA8},s=t.RGBA8),{r8unorm:t.R8,r8snorm:t.R8_SNORM,r8uint:t.R8UI,r8sint:t.R8I,r16uint:t.R16UI,r16sint:t.R16I,r16float:t.R16F,rg8unorm:t.RG8,rg8snorm:t.RG8_SNORM,rg8uint:t.RG8UI,rg8sint:t.RG8I,r32uint:t.R32UI,r32sint:t.R32I,r32float:t.R32F,rg16uint:t.RG16UI,rg16sint:t.RG16I,rg16float:t.RG16F,rgba8unorm:t.RGBA,...r,rgba8snorm:t.RGBA8_SNORM,rgba8uint:t.RGBA8UI,rgba8sint:t.RGBA8I,bgra8unorm:s,rgb9e5ufloat:t.RGB9_E5,rgb10a2unorm:t.RGB10_A2,rg11b10ufloat:t.R11F_G11F_B10F,rg32uint:t.RG32UI,rg32sint:t.RG32I,rg32float:t.RG32F,rgba16uint:t.RGBA16UI,rgba16sint:t.RGBA16I,rgba16float:t.RGBA16F,rgba32uint:t.RGBA32UI,rgba32sint:t.RGBA32I,rgba32float:t.RGBA32F,stencil8:t.STENCIL_INDEX8,depth16unorm:t.DEPTH_COMPONENT16,depth24plus:t.DEPTH_COMPONENT24,"depth24plus-stencil8":t.DEPTH24_STENCIL8,depth32float:t.DEPTH_COMPONENT32F,"depth32float-stencil8":t.DEPTH32F_STENCIL8,...e.s3tc?{"bc1-rgba-unorm":e.s3tc.COMPRESSED_RGBA_S3TC_DXT1_EXT,"bc2-rgba-unorm":e.s3tc.COMPRESSED_RGBA_S3TC_DXT3_EXT,"bc3-rgba-unorm":e.s3tc.COMPRESSED_RGBA_S3TC_DXT5_EXT}:{},...e.s3tc_sRGB?{"bc1-rgba-unorm-srgb":e.s3tc_sRGB.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT,"bc2-rgba-unorm-srgb":e.s3tc_sRGB.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT,"bc3-rgba-unorm-srgb":e.s3tc_sRGB.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT}:{},...e.rgtc?{"bc4-r-unorm":e.rgtc.COMPRESSED_RED_RGTC1_EXT,"bc4-r-snorm":e.rgtc.COMPRESSED_SIGNED_RED_RGTC1_EXT,"bc5-rg-unorm":e.rgtc.COMPRESSED_RED_GREEN_RGTC2_EXT,"bc5-rg-snorm":e.rgtc.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT}:{},...e.bptc?{"bc6h-rgb-float":e.bptc.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT,"bc6h-rgb-ufloat":e.bptc.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT,"bc7-rgba-unorm":e.bptc.COMPRESSED_RGBA_BPTC_UNORM_EXT,"bc7-rgba-unorm-srgb":e.bptc.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT}:{},...e.etc?{"etc2-rgb8unorm":e.etc.COMPRESSED_RGB8_ETC2,"etc2-rgb8unorm-srgb":e.etc.COMPRESSED_SRGB8_ETC2,"etc2-rgb8a1unorm":e.etc.COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2,"etc2-rgb8a1unorm-srgb":e.etc.COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2,"etc2-rgba8unorm":e.etc.COMPRESSED_RGBA8_ETC2_EAC,"etc2-rgba8unorm-srgb":e.etc.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC,"eac-r11unorm":e.etc.COMPRESSED_R11_EAC,"eac-rg11unorm":e.etc.COMPRESSED_SIGNED_RG11_EAC}:{},...e.astc?{"astc-4x4-unorm":e.astc.COMPRESSED_RGBA_ASTC_4x4_KHR,"astc-4x4-unorm-srgb":e.astc.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR,"astc-5x4-unorm":e.astc.COMPRESSED_RGBA_ASTC_5x4_KHR,"astc-5x4-unorm-srgb":e.astc.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR,"astc-5x5-unorm":e.astc.COMPRESSED_RGBA_ASTC_5x5_KHR,"astc-5x5-unorm-srgb":e.astc.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR,"astc-6x5-unorm":e.astc.COMPRESSED_RGBA_ASTC_6x5_KHR,"astc-6x5-unorm-srgb":e.astc.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR,"astc-6x6-unorm":e.astc.COMPRESSED_RGBA_ASTC_6x6_KHR,"astc-6x6-unorm-srgb":e.astc.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR,"astc-8x5-unorm":e.astc.COMPRESSED_RGBA_ASTC_8x5_KHR,"astc-8x5-unorm-srgb":e.astc.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR,"astc-8x6-unorm":e.astc.COMPRESSED_RGBA_ASTC_8x6_KHR,"astc-8x6-unorm-srgb":e.astc.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR,"astc-8x8-unorm":e.astc.COMPRESSED_RGBA_ASTC_8x8_KHR,"astc-8x8-unorm-srgb":e.astc.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR,"astc-10x5-unorm":e.astc.COMPRESSED_RGBA_ASTC_10x5_KHR,"astc-10x5-unorm-srgb":e.astc.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR,"astc-10x6-unorm":e.astc.COMPRESSED_RGBA_ASTC_10x6_KHR,"astc-10x6-unorm-srgb":e.astc.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR,"astc-10x8-unorm":e.astc.COMPRESSED_RGBA_ASTC_10x8_KHR,"astc-10x8-unorm-srgb":e.astc.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR,"astc-10x10-unorm":e.astc.COMPRESSED_RGBA_ASTC_10x10_KHR,"astc-10x10-unorm-srgb":e.astc.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR,"astc-12x10-unorm":e.astc.COMPRESSED_RGBA_ASTC_12x10_KHR,"astc-12x10-unorm-srgb":e.astc.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR,"astc-12x12-unorm":e.astc.COMPRESSED_RGBA_ASTC_12x12_KHR,"astc-12x12-unorm-srgb":e.astc.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR}:{}}}function yt(t){return{r8unorm:t.UNSIGNED_BYTE,r8snorm:t.BYTE,r8uint:t.UNSIGNED_BYTE,r8sint:t.BYTE,r16uint:t.UNSIGNED_SHORT,r16sint:t.SHORT,r16float:t.HALF_FLOAT,rg8unorm:t.UNSIGNED_BYTE,rg8snorm:t.BYTE,rg8uint:t.UNSIGNED_BYTE,rg8sint:t.BYTE,r32uint:t.UNSIGNED_INT,r32sint:t.INT,r32float:t.FLOAT,rg16uint:t.UNSIGNED_SHORT,rg16sint:t.SHORT,rg16float:t.HALF_FLOAT,rgba8unorm:t.UNSIGNED_BYTE,"rgba8unorm-srgb":t.UNSIGNED_BYTE,rgba8snorm:t.BYTE,rgba8uint:t.UNSIGNED_BYTE,rgba8sint:t.BYTE,bgra8unorm:t.UNSIGNED_BYTE,"bgra8unorm-srgb":t.UNSIGNED_BYTE,rgb9e5ufloat:t.UNSIGNED_INT_5_9_9_9_REV,rgb10a2unorm:t.UNSIGNED_INT_2_10_10_10_REV,rg11b10ufloat:t.UNSIGNED_INT_10F_11F_11F_REV,rg32uint:t.UNSIGNED_INT,rg32sint:t.INT,rg32float:t.FLOAT,rgba16uint:t.UNSIGNED_SHORT,rgba16sint:t.SHORT,rgba16float:t.HALF_FLOAT,rgba32uint:t.UNSIGNED_INT,rgba32sint:t.INT,rgba32float:t.FLOAT,stencil8:t.UNSIGNED_BYTE,depth16unorm:t.UNSIGNED_SHORT,depth24plus:t.UNSIGNED_INT,"depth24plus-stencil8":t.UNSIGNED_INT_24_8,depth32float:t.FLOAT,"depth32float-stencil8":t.FLOAT_32_UNSIGNED_INT_24_8_REV}}const Ct=4;class Se{constructor(e){this.managedTextures=[],this._glTextures=Object.create(null),this._glSamplers=Object.create(null),this._boundTextures=[],this._activeTextureLocation=-1,this._boundSamplers=Object.create(null),this._uploads={image:be,buffer:gt,video:Tt,compressed:xt},this._premultiplyAlpha=!1,this._useSeparateSamplers=!1,this._renderer=e,this._renderer.renderableGC.addManagedHash(this,"_glTextures"),this._renderer.renderableGC.addManagedHash(this,"_glSamplers")}contextChange(e){this._gl=e,this._mapFormatToInternalFormat||(this._mapFormatToInternalFormat=Nt(e,this._renderer.context.extensions),this._mapFormatToType=yt(e),this._mapFormatToFormat=At(e)),this._glTextures=Object.create(null),this._glSamplers=Object.create(null),this._boundSamplers=Object.create(null),this._premultiplyAlpha=!1;for(let r=0;r<16;r++)this.bind(b.EMPTY,r)}initSource(e){this.bind(e)}bind(e,r=0){const s=e.source;e?(this.bindSource(s,r),this._useSeparateSamplers&&this._bindSampler(s.style,r)):(this.bindSource(null,r),this._useSeparateSamplers&&this._bindSampler(null,r))}bindSource(e,r=0){const s=this._gl;if(e._touched=this._renderer.textureGC.count,this._boundTextures[r]!==e){this._boundTextures[r]=e,this._activateLocation(r),e||(e=b.EMPTY.source);const n=this.getGlSource(e);s.bindTexture(n.target,n.texture)}}_bindSampler(e,r=0){const s=this._gl;if(!e){this._boundSamplers[r]=null,s.bindSampler(r,null);return}const n=this._getGlSampler(e);this._boundSamplers[r]!==n&&(this._boundSamplers[r]=n,s.bindSampler(r,n))}unbind(e){const r=e.source,s=this._boundTextures,n=this._gl;for(let i=0;i1,this._renderer.context.extensions.anisotropicFiltering,"texParameteri",s.TEXTURE_2D,!this._renderer.context.supports.nonPowOf2wrapping&&!e.isPowerOfTwo,r)}onSourceUnload(e){const r=this._glTextures[e.uid];r&&(this.unbind(e),this._glTextures[e.uid]=null,this._gl.deleteTexture(r.texture))}onSourceUpdate(e){const r=this._gl,s=this.getGlSource(e);r.bindTexture(r.TEXTURE_2D,s.texture),this._boundTextures[this._activeTextureLocation]=e;const n=e.alphaMode==="premultiply-alpha-on-upload";this._premultiplyAlpha!==n&&(this._premultiplyAlpha=n,r.pixelStorei(r.UNPACK_PREMULTIPLY_ALPHA_WEBGL,n)),this._uploads[e.uploadMethodId]?this._uploads[e.uploadMethodId].upload(e,s,r,this._renderer.context.webGLVersion):r.texImage2D(r.TEXTURE_2D,0,r.RGBA,e.pixelWidth,e.pixelHeight,0,r.RGBA,r.UNSIGNED_BYTE,null),e.autoGenerateMipmaps&&e.mipLevelCount>1&&this.onUpdateMipmaps(e,!1)}onUpdateMipmaps(e,r=!0){r&&this.bindSource(e,0);const s=this.getGlSource(e);this._gl.generateMipmap(s.target)}onSourceDestroy(e){e.off("destroy",this.onSourceDestroy,this),e.off("update",this.onSourceUpdate,this),e.off("resize",this.onSourceUpdate,this),e.off("unload",this.onSourceUnload,this),e.off("styleChange",this.onStyleChange,this),e.off("updateMipmaps",this.onUpdateMipmaps,this),this.managedTextures.splice(this.managedTextures.indexOf(e),1),this.onSourceUnload(e)}_initSampler(e){const r=this._gl,s=this._gl.createSampler();return this._glSamplers[e._resourceId]=s,j(e,r,this._boundTextures[this._activeTextureLocation].mipLevelCount>1,this._renderer.context.extensions.anisotropicFiltering,"samplerParameteri",s,!1,!0),this._glSamplers[e._resourceId]}_getGlSampler(e){return this._glSamplers[e._resourceId]||this._initSampler(e)}getGlSource(e){return this._glTextures[e.uid]||this._initSource(e)}generateCanvas(e){const{pixels:r,width:s,height:n}=this.getPixels(e),i=S.get().createCanvas();i.width=s,i.height=n;const a=i.getContext("2d");if(a){const o=a.createImageData(s,n);o.data.set(r),a.putImageData(o,0,0)}return i}getPixels(e){const r=e.source.resolution,s=e.frame,n=Math.max(Math.round(s.width*r),1),i=Math.max(Math.round(s.height*r),1),a=new Uint8Array(Ct*n*i),o=this._renderer,c=o.renderTarget.getRenderTarget(e),u=o.renderTarget.getGpuRenderTarget(c),_=o.gl;return _.bindFramebuffer(_.FRAMEBUFFER,u.resolveTargetFramebuffer),_.readPixels(Math.round(s.x*r),Math.round(s.y*r),n,i,_.RGBA,_.UNSIGNED_BYTE,a),{pixels:new Uint8ClampedArray(a.buffer),width:n,height:i}}destroy(){this.managedTextures.slice().forEach(e=>this.onSourceDestroy(e)),this.managedTextures=null,this._glTextures=null,this._glSamplers=null,this._boundTextures=null,this._boundSamplers=null,this._mapFormatToInternalFormat=null,this._mapFormatToType=null,this._mapFormatToFormat=null,this._uploads=null,this._renderer=null}resetState(){this._activeTextureLocation=-1,this._boundTextures.fill(b.EMPTY.source),this._boundSamplers=Object.create(null);const e=this._gl;this._premultiplyAlpha=!1,e.pixelStorei(e.UNPACK_PREMULTIPLY_ALPHA_WEBGL,this._premultiplyAlpha)}}Se.extension={type:[d.WebGLSystem],name:"texture"};class pe{contextChange(e){const r=new F({uColor:{value:new Float32Array([1,1,1,1]),type:"vec4"},uTransformMatrix:{value:new z,type:"mat3x3"},uRound:{value:0,type:"f32"}}),s=e.limits.maxBatchableTextures,n=Y({name:"graphics",bits:[ye,Ce(s),Z,q]});this.shader=new U({glProgram:n,resources:{localUniforms:r,batchSamplers:Ie(s)}})}execute(e,r){const s=r.context,n=s.customShader||this.shader,i=e.renderer,a=i.graphicsContext,{batcher:o,instructions:c}=a.getContextRenderData(s);n.groups[0]=i.globalUniforms.bindGroup,i.state.set(e.state),i.shader.bind(n),i.geometry.bind(o.geometry,n.glProgram);const u=c.instructions;for(let _=0;_",value:new z}}}})}execute(e,r){const s=e.renderer;let n=r._shader;if(n){if(!n.glProgram){m("Mesh shader has no glProgram",r.shader);return}}else{n=this._shader;const i=r.texture,a=i.source;n.resources.uTexture=a,n.resources.uSampler=a.style,n.resources.textureUniforms.uniforms.uTextureMatrix=i.textureMatrix.mapCoord}n.groups[100]=s.globalUniforms.bindGroup,n.groups[101]=e.localUniformsBindGroup,s.encoder.draw({geometry:r._geometry,shader:n,state:r.state})}destroy(){this._shader.destroy(!0),this._shader=null}}ge.extension={type:[d.WebGLPipesAdaptor],name:"mesh"};const It=[...we,_e,$e,Xe,oe,ee,Se,he,se,me,de,ae,St,ce,ie],Dt=[...Ve],Gt=[Q,ge,pe],Re=[],xe=[],Te=[];T.handleByNamedList(d.WebGLSystem,Re);T.handleByNamedList(d.WebGLPipes,xe);T.handleByNamedList(d.WebGLPipesAdaptor,Te);T.add(...It,...Dt,...Gt);class Pt extends De{constructor(){const e={name:"webgl",type:Ge.WEBGL,systems:Re,renderPipes:xe,renderPipeAdaptors:Te};super(e)}}export{Pt as WebGLRenderer}; diff --git a/dist/assets/WebGPURenderer-CtyVoQqf.js b/dist/assets/WebGPURenderer-CtyVoQqf.js new file mode 100644 index 0000000..f626925 --- /dev/null +++ b/dist/assets/WebGPURenderer-CtyVoQqf.js @@ -0,0 +1,41 @@ +import{g as E,E as p,f as ae,D as B,S as C,B as T,b as ue,c as L,d as M,w as v,h as y,i as ce,j as de,k as A,l as w,M as k,m as D,n as he,o as pe,p as H,q as z,s as R,A as le,R as fe,e as S}from"./index-DB6B0XuI.js";import{S as F,l as ge,a as me}from"./colorToUniform-DmtBy-2V.js";import{c as _e,u as be,U as ye,B as xe,G as Ge,e as Be,R as Se,t as Pe,S as Te,a as Ce}from"./SharedSystems-DVK37F7d.js";const x=F.for2d();class O{start(e,t,r){const s=e.renderer,i=s.encoder,n=r.gpuProgram;this._shader=r,this._geometry=t,i.setGeometry(t,n),x.blendMode="normal",s.pipeline.getPipeline(t,n,x);const o=s.globalUniforms.bindGroup;i.resetBindGroup(1),i.setBindGroup(0,o,n)}execute(e,t){const r=this._shader.gpuProgram,s=e.renderer,i=s.encoder;if(!t.bindGroup){const u=t.textures;t.bindGroup=E(u.textures,u.count,s.limits.maxBatchableTextures)}x.blendMode=t.blendMode;const n=s.bindGroup.getBindGroup(t.bindGroup,r,1),o=s.pipeline.getPipeline(this._geometry,r,x,t.topology);t.bindGroup._touch(s.textureGC.count),i.setPipeline(o),i.renderPassEncoder.setBindGroup(1,n),i.renderPassEncoder.drawIndexed(t.size,1,t.start)}}O.extension={type:[p.WebGPUPipesAdaptor],name:"batch"};class I{constructor(e){this._hash=Object.create(null),this._renderer=e,this._renderer.renderableGC.addManagedHash(this,"_hash")}contextChange(e){this._gpu=e}getBindGroup(e,t,r){return e._updateKey(),this._hash[e._key]||this._createBindGroup(e,t,r)}_createBindGroup(e,t,r){const s=this._gpu.device,i=t.layout[r],n=[],o=this._renderer;for(const l in i){const h=e.resources[l]??e.resources[i[l]];let f;if(h._resourceType==="uniformGroup"){const d=h;o.ubo.updateUniformGroup(d);const _=d.buffer;f={buffer:o.buffer.getGPUBuffer(_),offset:0,size:_.descriptor.size}}else if(h._resourceType==="buffer"){const d=h;f={buffer:o.buffer.getGPUBuffer(d),offset:0,size:d.descriptor.size}}else if(h._resourceType==="bufferResource"){const d=h;f={buffer:o.buffer.getGPUBuffer(d.buffer),offset:d.offset,size:d.size}}else if(h._resourceType==="textureSampler"){const d=h;f=o.texture.getGpuSampler(d)}else if(h._resourceType==="textureSource"){const d=h;f=o.texture.getGpuSource(d).createView({})}n.push({binding:i[l],resource:f})}const u=o.shader.getProgramData(t).bindGroups[r],c=s.createBindGroup({layout:u,entries:n});return this._hash[e._key]=c,c}destroy(){for(const e of Object.keys(this._hash))this._hash[e]=null;this._hash=null,this._renderer=null}}I.extension={type:[p.WebGPUSystem],name:"bindGroup"};class W{constructor(e){this._gpuBuffers=Object.create(null),this._managedBuffers=[],e.renderableGC.addManagedHash(this,"_gpuBuffers")}contextChange(e){this._gpu=e}getGPUBuffer(e){return this._gpuBuffers[e.uid]||this.createGPUBuffer(e)}updateBuffer(e){const t=this._gpuBuffers[e.uid]||this.createGPUBuffer(e),r=e.data;return e._updateID&&r&&(e._updateID=0,this._gpu.device.queue.writeBuffer(t,0,r.buffer,0,(e._updateSize||r.byteLength)+3&-4)),t}destroyAll(){for(const e in this._gpuBuffers)this._gpuBuffers[e].destroy();this._gpuBuffers={}}createGPUBuffer(e){this._gpuBuffers[e.uid]||(e.on("update",this.updateBuffer,this),e.on("change",this.onBufferChange,this),e.on("destroy",this.onBufferDestroy,this),this._managedBuffers.push(e));const t=this._gpu.device.createBuffer(e.descriptor);return e._updateID=0,e.data&&(ae(e.data.buffer,t.getMappedRange()),t.unmap()),this._gpuBuffers[e.uid]=t,t}onBufferChange(e){this._gpuBuffers[e.uid].destroy(),e._updateID=0,this._gpuBuffers[e.uid]=this.createGPUBuffer(e)}onBufferDestroy(e){this._managedBuffers.splice(this._managedBuffers.indexOf(e),1),this._destroyBuffer(e)}destroy(){this._managedBuffers.forEach(e=>this._destroyBuffer(e)),this._managedBuffers=null,this._gpuBuffers=null}_destroyBuffer(e){this._gpuBuffers[e.uid].destroy(),e.off("update",this.updateBuffer,this),e.off("change",this.onBufferChange,this),e.off("destroy",this.onBufferDestroy,this),this._gpuBuffers[e.uid]=null}}W.extension={type:[p.WebGPUSystem],name:"buffer"};class ve{constructor({minUniformOffsetAlignment:e}){this._minUniformOffsetAlignment=256,this.byteIndex=0,this._minUniformOffsetAlignment=e,this.data=new Float32Array(65535)}clear(){this.byteIndex=0}addEmptyGroup(e){if(e>this._minUniformOffsetAlignment/4)throw new Error(`UniformBufferBatch: array is too large: ${e*4}`);const t=this.byteIndex;let r=t+e*4;if(r=Math.ceil(r/this._minUniformOffsetAlignment)*this._minUniformOffsetAlignment,r>this.data.length*4)throw new Error("UniformBufferBatch: ubo batch got too big");return this.byteIndex=r,t}addGroup(e){const t=this.addEmptyGroup(e.length);for(let r=0;r{this.gpu=t,this._renderer.runners.contextChange.emit(this.gpu)}),this._initPromise)}contextChange(e){this._renderer.gpu=e}async _createDeviceAndAdaptor(e){const t=await B.get().getNavigator().gpu.requestAdapter({powerPreference:e.powerPreference,forceFallbackAdapter:e.forceFallbackAdapter}),r=["texture-compression-bc","texture-compression-astc","texture-compression-etc2"].filter(i=>t.features.has(i)),s=await t.requestDevice({requiredFeatures:r});return{adapter:t,device:s}}destroy(){this.gpu=null,this._renderer=null}}U.extension={type:[p.WebGPUSystem],name:"device"};U.defaultOptions={powerPreference:void 0,forceFallbackAdapter:!1};class N{constructor(e){this._boundBindGroup=Object.create(null),this._boundVertexBuffer=Object.create(null),this._renderer=e}renderStart(){this.commandFinished=new Promise(e=>{this._resolveCommandFinished=e}),this.commandEncoder=this._renderer.gpu.device.createCommandEncoder()}beginRenderPass(e){this.endRenderPass(),this._clearCache(),this.renderPassEncoder=this.commandEncoder.beginRenderPass(e.descriptor)}endRenderPass(){this.renderPassEncoder&&this.renderPassEncoder.end(),this.renderPassEncoder=null}setViewport(e){this.renderPassEncoder.setViewport(e.x,e.y,e.width,e.height,0,1)}setPipelineFromGeometryProgramAndState(e,t,r,s){const i=this._renderer.pipeline.getPipeline(e,t,r,s);this.setPipeline(i)}setPipeline(e){this._boundPipeline!==e&&(this._boundPipeline=e,this.renderPassEncoder.setPipeline(e))}_setVertexBuffer(e,t){this._boundVertexBuffer[e]!==t&&(this._boundVertexBuffer[e]=t,this.renderPassEncoder.setVertexBuffer(e,this._renderer.buffer.updateBuffer(t)))}_setIndexBuffer(e){if(this._boundIndexBuffer===e)return;this._boundIndexBuffer=e;const t=e.data.BYTES_PER_ELEMENT===2?"uint16":"uint32";this.renderPassEncoder.setIndexBuffer(this._renderer.buffer.updateBuffer(e),t)}resetBindGroup(e){this._boundBindGroup[e]=null}setBindGroup(e,t,r){if(this._boundBindGroup[e]===t)return;this._boundBindGroup[e]=t,t._touch(this._renderer.textureGC.count);const s=this._renderer.bindGroup.getBindGroup(t,r,e);this.renderPassEncoder.setBindGroup(e,s)}setGeometry(e,t){const r=this._renderer.pipeline.getBufferNamesToBind(e,t);for(const s in r)this._setVertexBuffer(parseInt(s,10),e.attributes[r[s]].buffer);e.indexBuffer&&this._setIndexBuffer(e.indexBuffer)}_setShaderBindGroups(e,t){for(const r in e.groups){const s=e.groups[r];t||this._syncBindGroup(s),this.setBindGroup(r,s,e.gpuProgram)}}_syncBindGroup(e){for(const t in e.resources){const r=e.resources[t];r.isUniformGroup&&this._renderer.ubo.updateUniformGroup(r)}}draw(e){const{geometry:t,shader:r,state:s,topology:i,size:n,start:o,instanceCount:u,skipSync:c}=e;this.setPipelineFromGeometryProgramAndState(t,r.gpuProgram,s,i),this.setGeometry(t,r.gpuProgram),this._setShaderBindGroups(r,c),t.indexBuffer?this.renderPassEncoder.drawIndexed(n||t.indexBuffer.data.length,u??t.instanceCount,o||0):this.renderPassEncoder.draw(n||t.getSize(),u??t.instanceCount,o||0)}finishRenderPass(){this.renderPassEncoder&&(this.renderPassEncoder.end(),this.renderPassEncoder=null)}postrender(){this.finishRenderPass(),this._gpu.device.queue.submit([this.commandEncoder.finish()]),this._resolveCommandFinished(),this.commandEncoder=null}restoreRenderPass(){const e=this._renderer.renderTarget.adaptor.getDescriptor(this._renderer.renderTarget.renderTarget,!1,[0,0,0,1]);this.renderPassEncoder=this.commandEncoder.beginRenderPass(e);const t=this._boundPipeline,r={...this._boundVertexBuffer},s=this._boundIndexBuffer,i={...this._boundBindGroup};this._clearCache();const n=this._renderer.renderTarget.viewport;this.renderPassEncoder.setViewport(n.x,n.y,n.width,n.height,0,1),this.setPipeline(t);for(const o in r)this._setVertexBuffer(o,r[o]);for(const o in i)this.setBindGroup(o,i[o],null);this._setIndexBuffer(s)}_clearCache(){for(let e=0;e<16;e++)this._boundBindGroup[e]=null,this._boundVertexBuffer[e]=null;this._boundIndexBuffer=null,this._boundPipeline=null}destroy(){this._renderer=null,this._gpu=null,this._boundBindGroup=null,this._boundVertexBuffer=null,this._boundIndexBuffer=null,this._boundPipeline=null}contextChange(e){this._gpu=e}}N.extension={type:[p.WebGPUSystem],name:"encoder",priority:1};class j{constructor(e){this._renderer=e}contextChange(){this.maxTextures=this._renderer.device.gpu.device.limits.maxSampledTexturesPerShaderStage,this.maxBatchableTextures=this.maxTextures}destroy(){}}j.extension={type:[p.WebGPUSystem],name:"limits"};class K{constructor(e){this._renderTargetStencilState=Object.create(null),this._renderer=e,e.renderTarget.onRenderTargetChange.add(this)}onRenderTargetChange(e){let t=this._renderTargetStencilState[e.uid];t||(t=this._renderTargetStencilState[e.uid]={stencilMode:C.DISABLED,stencilReference:0}),this._activeRenderTarget=e,this.setStencilMode(t.stencilMode,t.stencilReference)}setStencilMode(e,t){const r=this._renderTargetStencilState[this._activeRenderTarget.uid];r.stencilMode=e,r.stencilReference=t;const s=this._renderer;s.pipeline.setStencilMode(e),s.encoder.renderPassEncoder.setStencilReference(t)}destroy(){this._renderer.renderTarget.onRenderTargetChange.remove(this),this._renderer=null,this._activeRenderTarget=null,this._renderTargetStencilState=null}}K.extension={type:[p.WebGPUSystem],name:"stencil"};const G={i32:{align:4,size:4},u32:{align:4,size:4},f32:{align:4,size:4},f16:{align:2,size:2},"vec2":{align:8,size:8},"vec2":{align:8,size:8},"vec2":{align:8,size:8},"vec2":{align:4,size:4},"vec3":{align:16,size:12},"vec3":{align:16,size:12},"vec3":{align:16,size:12},"vec3":{align:8,size:6},"vec4":{align:16,size:16},"vec4":{align:16,size:16},"vec4":{align:16,size:16},"vec4":{align:8,size:8},"mat2x2":{align:8,size:16},"mat2x2":{align:4,size:8},"mat3x2":{align:8,size:24},"mat3x2":{align:4,size:12},"mat4x2":{align:8,size:32},"mat4x2":{align:4,size:16},"mat2x3":{align:16,size:32},"mat2x3":{align:8,size:16},"mat3x3":{align:16,size:48},"mat3x3":{align:8,size:24},"mat4x3":{align:16,size:64},"mat4x3":{align:8,size:32},"mat2x4":{align:16,size:32},"mat2x4":{align:8,size:16},"mat3x4":{align:16,size:48},"mat3x4":{align:8,size:24},"mat4x4":{align:16,size:64},"mat4x4":{align:8,size:32}};function Ue(a){const e=a.map(r=>({data:r,offset:0,size:0}));let t=0;for(let r=0;r1&&(i=Math.max(i,n)*s.data.size),t=Math.ceil(t/n)*n,s.size=i,s.offset=t,t+=i}return t=Math.ceil(t/16)*16,{uboElements:e,size:t}}function Me(a,e){const{size:t,align:r}=G[a.data.type],s=(r-t)/4,i=a.data.type.indexOf("i32")>=0?"dataInt32":"data";return` + v = uv.${a.data.name}; + ${e!==0?`offset += ${e};`:""} + + arrayOffset = offset; + + t = 0; + + for(var i=0; i < ${a.data.size*(t/4)}; i++) + { + for(var j = 0; j < ${t/4}; j++) + { + ${i}[arrayOffset++] = v[t++]; + } + ${s!==0?`arrayOffset += ${s};`:""} + } + `}function we(a){return _e(a,"uboWgsl",Me,be)}class q extends ye{constructor(){super({createUboElements:Ue,generateUboSync:we})}}q.extension={type:[p.WebGPUSystem],name:"ubo"};const b=128;class Y{constructor(e){this._bindGroupHash=Object.create(null),this._buffers=[],this._bindGroups=[],this._bufferResources=[],this._renderer=e,this._renderer.renderableGC.addManagedHash(this,"_bindGroupHash"),this._batchBuffer=new ve({minUniformOffsetAlignment:b});const t=256/b;for(let r=0;r{const n={arrayStride:0,stepMode:"vertex",attributes:[]},o=n.attributes;for(const u in t.attributeData){const c=e.attributes[u];(c.divisor??1)!==1&&v(`Attribute ${u} has an invalid divisor value of '${c.divisor}'. WebGPU only supports a divisor value of 1`),c.buffer===i&&(n.arrayStride=c.stride,n.stepMode=c.instance?"instance":"vertex",o.push({shaderLocation:t.attributeData[u].location,offset:c.offset,format:c.format}))}o.length&&s.push(n)}),this._bufferLayoutsCache[r]=s,s}_updatePipeHash(){const e=Le(this._stencilMode,this._multisampleCount,this._colorMask,this._depthStencilAttachment);this._pipeStateCaches[e]||(this._pipeStateCaches[e]=Object.create(null)),this._pipeCache=this._pipeStateCaches[e]}destroy(){this._renderer=null,this._bufferLayoutsCache=null}}$.extension={type:[p.WebGPUSystem],name:"pipeline"};class Ae{constructor(){this.contexts=[],this.msaaTextures=[],this.msaaSamples=1}}class ke{init(e,t){this._renderer=e,this._renderTargetSystem=t}copyToTexture(e,t,r,s,i){const n=this._renderer,o=this._getGpuColorTexture(e),u=n.texture.getGpuSource(t.source);return n.encoder.commandEncoder.copyTextureToTexture({texture:o,origin:r},{texture:u,origin:i},s),t}startRenderPass(e,t=!0,r,s){const n=this._renderTargetSystem.getGpuRenderTarget(e),o=this.getDescriptor(e,t,r);n.descriptor=o,this._renderer.pipeline.setRenderTarget(n),this._renderer.encoder.beginRenderPass(n),this._renderer.encoder.setViewport(s)}finishRenderPass(){this._renderer.encoder.endRenderPass()}_getGpuColorTexture(e){const t=this._renderTargetSystem.getGpuRenderTarget(e);return t.contexts[0]?t.contexts[0].getCurrentTexture():this._renderer.texture.getGpuSource(e.colorTextures[0].source)}getDescriptor(e,t,r){typeof t=="boolean"&&(t=t?y.ALL:y.NONE);const s=this._renderTargetSystem,i=s.getGpuRenderTarget(e),n=e.colorTextures.map((c,l)=>{const h=i.contexts[l];let f,d;h?f=h.getCurrentTexture().createView():f=this._renderer.texture.getGpuSource(c).createView({mipLevelCount:1}),i.msaaTextures[l]&&(d=f,f=this._renderer.texture.getTextureView(i.msaaTextures[l]));const _=t&y.COLOR?"clear":"load";return r??(r=s.defaultClearColor),{view:f,resolveTarget:d,clearValue:r,storeOp:"store",loadOp:_}});let o;if((e.stencil||e.depth)&&!e.depthStencilTexture&&(e.ensureDepthStencilTexture(),e.depthStencilTexture.source.sampleCount=i.msaa?4:1),e.depthStencilTexture){const c=t&y.STENCIL?"clear":"load",l=t&y.DEPTH?"clear":"load";o={view:this._renderer.texture.getGpuSource(e.depthStencilTexture.source).createView(),stencilStoreOp:"store",stencilLoadOp:c,depthClearValue:1,depthLoadOp:l,depthStoreOp:"store"}}return{colorAttachments:n,depthStencilAttachment:o}}clear(e,t=!0,r,s){if(!t)return;const{gpu:i,encoder:n}=this._renderer,o=i.device;if(n.commandEncoder===null){const c=o.createCommandEncoder(),l=this.getDescriptor(e,t,r),h=c.beginRenderPass(l);h.setViewport(s.x,s.y,s.width,s.height,0,1),h.end();const f=c.finish();o.queue.submit([f])}else this.startRenderPass(e,t,r,s)}initGpuRenderTarget(e){e.isRoot=!0;const t=new Ae;return e.colorTextures.forEach((r,s)=>{if(r instanceof ce){const i=r.resource.getContext("webgpu"),n=r.transparent?"premultiplied":"opaque";try{i.configure({device:this._renderer.gpu.device,usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST|GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.COPY_SRC,format:"bgra8unorm",alphaMode:n})}catch(o){console.error(o)}t.contexts[s]=i}if(t.msaa=r.source.antialias,r.source.antialias){const i=new de({width:0,height:0,sampleCount:4});t.msaaTextures[s]=i}}),t.msaa&&(t.msaaSamples=4,e.depthStencilTexture&&(e.depthStencilTexture.source.sampleCount=4)),t}destroyGpuRenderTarget(e){e.contexts.forEach(t=>{t.unconfigure()}),e.msaaTextures.forEach(t=>{t.destroy()}),e.msaaTextures.length=0,e.contexts.length=0}ensureDepthStencilTexture(e){const t=this._renderTargetSystem.getGpuRenderTarget(e);e.depthStencilTexture&&t.msaa&&(e.depthStencilTexture.source.sampleCount=4)}resizeGpuRenderTarget(e){const t=this._renderTargetSystem.getGpuRenderTarget(e);t.width=e.width,t.height=e.height,t.msaa&&e.colorTextures.forEach((r,s)=>{const i=t.msaaTextures[s];i==null||i.resize(r.source.width,r.source.height,r.source._resolution)})}}class X extends Se{constructor(e){super(e),this.adaptor=new ke,this.adaptor.init(e,this)}}X.extension={type:[p.WebGPUSystem],name:"renderTarget"};class Z{constructor(){this._gpuProgramData=Object.create(null)}contextChange(e){this._gpu=e}getProgramData(e){return this._gpuProgramData[e._layoutKey]||this._createGPUProgramData(e)}_createGPUProgramData(e){const t=this._gpu.device,r=e.gpuLayout.map(i=>t.createBindGroupLayout({entries:i})),s={bindGroupLayouts:r};return this._gpuProgramData[e._layoutKey]={bindGroups:r,pipeline:t.createPipelineLayout(s)},this._gpuProgramData[e._layoutKey]}destroy(){this._gpu=null,this._gpuProgramData=null}}Z.extension={type:[p.WebGPUSystem],name:"shader"};const g={};g.normal={alpha:{srcFactor:"one",dstFactor:"one-minus-src-alpha",operation:"add"},color:{srcFactor:"one",dstFactor:"one-minus-src-alpha",operation:"add"}};g.add={alpha:{srcFactor:"src-alpha",dstFactor:"one-minus-src-alpha",operation:"add"},color:{srcFactor:"one",dstFactor:"one",operation:"add"}};g.multiply={alpha:{srcFactor:"one",dstFactor:"one-minus-src-alpha",operation:"add"},color:{srcFactor:"dst",dstFactor:"one-minus-src-alpha",operation:"add"}};g.screen={alpha:{srcFactor:"one",dstFactor:"one-minus-src-alpha",operation:"add"},color:{srcFactor:"one",dstFactor:"one-minus-src",operation:"add"}};g.overlay={alpha:{srcFactor:"one",dstFactor:"one-minus-src-alpha",operation:"add"},color:{srcFactor:"one",dstFactor:"one-minus-src",operation:"add"}};g.none={alpha:{srcFactor:"one",dstFactor:"one-minus-src-alpha",operation:"add"},color:{srcFactor:"zero",dstFactor:"zero",operation:"add"}};g["normal-npm"]={alpha:{srcFactor:"one",dstFactor:"one-minus-src-alpha",operation:"add"},color:{srcFactor:"src-alpha",dstFactor:"one-minus-src-alpha",operation:"add"}};g["add-npm"]={alpha:{srcFactor:"one",dstFactor:"one",operation:"add"},color:{srcFactor:"src-alpha",dstFactor:"one",operation:"add"}};g["screen-npm"]={alpha:{srcFactor:"one",dstFactor:"one-minus-src-alpha",operation:"add"},color:{srcFactor:"src-alpha",dstFactor:"one-minus-src",operation:"add"}};g.erase={alpha:{srcFactor:"zero",dstFactor:"one-minus-src-alpha",operation:"add"},color:{srcFactor:"zero",dstFactor:"one-minus-src",operation:"add"}};g.min={alpha:{srcFactor:"one",dstFactor:"one",operation:"min"},color:{srcFactor:"one",dstFactor:"one",operation:"min"}};g.max={alpha:{srcFactor:"one",dstFactor:"one",operation:"max"},color:{srcFactor:"one",dstFactor:"one",operation:"max"}};class J{constructor(){this.defaultState=new F,this.defaultState.blend=!0}contextChange(e){this.gpu=e}getColorTargets(e){return[{format:"bgra8unorm",writeMask:0,blend:g[e.blendMode]||g.normal}]}destroy(){this.gpu=null}}J.extension={type:[p.WebGPUSystem],name:"state"};const De={type:"image",upload(a,e,t){const r=a.resource,s=(a.pixelWidth|0)*(a.pixelHeight|0),i=r.byteLength/s;t.device.queue.writeTexture({texture:e},r,{offset:0,rowsPerImage:a.pixelHeight,bytesPerRow:a.pixelHeight*i},{width:a.pixelWidth,height:a.pixelHeight,depthOrArrayLayers:1})}},Q={"bc1-rgba-unorm":{blockBytes:8,blockWidth:4,blockHeight:4},"bc2-rgba-unorm":{blockBytes:16,blockWidth:4,blockHeight:4},"bc3-rgba-unorm":{blockBytes:16,blockWidth:4,blockHeight:4},"bc7-rgba-unorm":{blockBytes:16,blockWidth:4,blockHeight:4},"etc1-rgb-unorm":{blockBytes:8,blockWidth:4,blockHeight:4},"etc2-rgba8unorm":{blockBytes:16,blockWidth:4,blockHeight:4},"astc-4x4-unorm":{blockBytes:16,blockWidth:4,blockHeight:4}},He={blockBytes:4,blockWidth:1,blockHeight:1},ze={type:"compressed",upload(a,e,t){let r=a.pixelWidth,s=a.pixelHeight;const i=Q[a.format]||He;for(let n=0;n>1,1),s=Math.max(s>>1,1)}}},ee={type:"image",upload(a,e,t){const r=a.resource;if(!r)return;if(globalThis.HTMLImageElement&&r instanceof HTMLImageElement){const o=B.get().createCanvas(r.width,r.height);o.getContext("2d").drawImage(r,0,0,r.width,r.height),a.resource=o,v("ImageSource: Image element passed, converting to canvas and replacing resource.")}const s=Math.min(e.width,a.resourceWidth||a.pixelWidth),i=Math.min(e.height,a.resourceHeight||a.pixelHeight),n=a.alphaMode==="premultiply-alpha-on-upload";t.device.queue.copyExternalImageToTexture({source:r},{texture:e,premultipliedAlpha:n},{width:s,height:i})}},Fe={type:"video",upload(a,e,t){ee.upload(a,e,t)}};class Oe{constructor(e){this.device=e,this.sampler=e.createSampler({minFilter:"linear"}),this.pipelines={}}_getMipmapPipeline(e){let t=this.pipelines[e];return t||(this.mipmapShaderModule||(this.mipmapShaderModule=this.device.createShaderModule({code:` + var pos : array, 3> = array, 3>( + vec2(-1.0, -1.0), vec2(-1.0, 3.0), vec2(3.0, -1.0)); + + struct VertexOutput { + @builtin(position) position : vec4, + @location(0) texCoord : vec2, + }; + + @vertex + fn vertexMain(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput { + var output : VertexOutput; + output.texCoord = pos[vertexIndex] * vec2(0.5, -0.5) + vec2(0.5); + output.position = vec4(pos[vertexIndex], 0.0, 1.0); + return output; + } + + @group(0) @binding(0) var imgSampler : sampler; + @group(0) @binding(1) var img : texture_2d; + + @fragment + fn fragmentMain(@location(0) texCoord : vec2) -> @location(0) vec4 { + return textureSample(img, imgSampler, texCoord); + } + `})),t=this.device.createRenderPipeline({layout:"auto",vertex:{module:this.mipmapShaderModule,entryPoint:"vertexMain"},fragment:{module:this.mipmapShaderModule,entryPoint:"fragmentMain",targets:[{format:e}]}}),this.pipelines[e]=t),t}generateMipmap(e){const t=this._getMipmapPipeline(e.format);if(e.dimension==="3d"||e.dimension==="1d")throw new Error("Generating mipmaps for non-2d textures is currently unsupported!");let r=e;const s=e.depthOrArrayLayers||1,i=e.usage&GPUTextureUsage.RENDER_ATTACHMENT;if(!i){const u={size:{width:Math.ceil(e.width/2),height:Math.ceil(e.height/2),depthOrArrayLayers:s},format:e.format,usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_SRC|GPUTextureUsage.RENDER_ATTACHMENT,mipLevelCount:e.mipLevelCount-1};r=this.device.createTexture(u)}const n=this.device.createCommandEncoder({}),o=t.getBindGroupLayout(0);for(let u=0;u1&&this.onUpdateMipmaps(e))}onSourceUnload(e){const t=this._gpuSources[e.uid];t&&(this._gpuSources[e.uid]=null,t.destroy())}onUpdateMipmaps(e){this._mipmapGenerator||(this._mipmapGenerator=new Oe(this._gpu.device));const t=this.getGpuSource(e);this._mipmapGenerator.generateMipmap(t)}onSourceDestroy(e){e.off("update",this.onSourceUpdate,this),e.off("unload",this.onSourceUnload,this),e.off("destroy",this.onSourceDestroy,this),e.off("resize",this.onSourceResize,this),e.off("updateMipmaps",this.onUpdateMipmaps,this),this.managedTextures.splice(this.managedTextures.indexOf(e),1),this.onSourceUnload(e)}onSourceResize(e){const t=this._gpuSources[e.uid];t?(t.width!==e.pixelWidth||t.height!==e.pixelHeight)&&(this._textureViewHash[e.uid]=null,this._bindGroupHash[e.uid]=null,this.onSourceUnload(e),this.initSource(e)):this.initSource(e)}_initSampler(e){return this._gpuSamplers[e._resourceId]=this._gpu.device.createSampler(e),this._gpuSamplers[e._resourceId]}getGpuSampler(e){return this._gpuSamplers[e._resourceId]||this._initSampler(e)}getGpuSource(e){return this._gpuSources[e.uid]||this.initSource(e)}getTextureBindGroup(e){return this._bindGroupHash[e.uid]??this._createTextureBindGroup(e)}_createTextureBindGroup(e){const t=e.source;return this._bindGroupHash[e.uid]=new L({0:t,1:t.style,2:new A({uTextureMatrix:{type:"mat3x3",value:e.textureMatrix.mapCoord}})}),this._bindGroupHash[e.uid]}getTextureView(e){const t=e.source;return this._textureViewHash[t.uid]??this._createTextureView(t)}_createTextureView(e){return this._textureViewHash[e.uid]=this.getGpuSource(e).createView(),this._textureViewHash[e.uid]}generateCanvas(e){const t=this._renderer,r=t.gpu.device.createCommandEncoder(),s=B.get().createCanvas();s.width=e.source.pixelWidth,s.height=e.source.pixelHeight;const i=s.getContext("webgpu");return i.configure({device:t.gpu.device,usage:GPUTextureUsage.COPY_DST|GPUTextureUsage.COPY_SRC,format:B.get().getNavigator().gpu.getPreferredCanvasFormat(),alphaMode:"premultiplied"}),r.copyTextureToTexture({texture:t.texture.getGpuSource(e.source),origin:{x:0,y:0}},{texture:i.getCurrentTexture()},{width:s.width,height:s.height}),t.gpu.device.queue.submit([r.finish()]),s}getPixels(e){const t=this.generateCanvas(e),r=w.getOptimalCanvasAndContext(t.width,t.height),s=r.context;s.drawImage(t,0,0);const{width:i,height:n}=t,o=s.getImageData(0,0,i,n),u=new Uint8ClampedArray(o.data.buffer);return w.returnCanvasAndContext(r),{pixels:u,width:i,height:n}}destroy(){this.managedTextures.slice().forEach(e=>this.onSourceDestroy(e)),this.managedTextures=null;for(const e of Object.keys(this._bindGroupHash)){const t=Number(e),r=this._bindGroupHash[t];r==null||r.destroy(),this._bindGroupHash[t]=null}this._gpu=null,this._mipmapGenerator=null,this._gpuSources=null,this._bindGroupHash=null,this._textureViewHash=null,this._gpuSamplers=null}}te.extension={type:[p.WebGPUSystem],name:"texture"};class re{constructor(){this._maxTextures=0}contextChange(e){const t=new A({uTransformMatrix:{value:new k,type:"mat3x3"},uColor:{value:new Float32Array([1,1,1,1]),type:"vec4"},uRound:{value:0,type:"f32"}});this._maxTextures=e.limits.maxBatchableTextures;const r=D({name:"graphics",bits:[he,pe(this._maxTextures),ge,H]});this.shader=new z({gpuProgram:r,resources:{localUniforms:t}})}execute(e,t){const r=t.context,s=r.customShader||this.shader,i=e.renderer,n=i.graphicsContext,{batcher:o,instructions:u}=n.getContextRenderData(r),c=i.encoder;c.setGeometry(o.geometry,s.gpuProgram);const l=i.globalUniforms.bindGroup;c.setBindGroup(0,l,s.gpuProgram);const h=i.renderPipes.uniformBatch.getUniformBindGroup(s.resources.localUniforms,!0);c.setBindGroup(2,h,s.gpuProgram);const f=u.instructions;let d=null;for(let _=0;_",value:new k}}}})}execute(e,t){const r=e.renderer;let s=t._shader;if(!s)s=this._shader,s.groups[2]=r.texture.getTextureBindGroup(t.texture);else if(!s.gpuProgram){v("Mesh shader has no gpuProgram",t.shader);return}const i=s.gpuProgram;if(i.autoAssignGlobalUniforms&&(s.groups[0]=r.globalUniforms.bindGroup),i.autoAssignLocalUniforms){const n=e.localUniforms;s.groups[1]=r.renderPipes.uniformBatch.getUniformBindGroup(n,!0)}r.encoder.draw({geometry:t._geometry,shader:s,state:t.state})}destroy(){this._shader.destroy(!0),this._shader=null}}se.extension={type:[p.WebGPUPipesAdaptor],name:"mesh"};const Ie=[...Te,q,N,U,j,W,te,X,Z,J,$,V,K,I],We=[...Ce,Y],Ve=[O,se,re],ie=[],ne=[],oe=[];S.handleByNamedList(p.WebGPUSystem,ie);S.handleByNamedList(p.WebGPUPipes,ne);S.handleByNamedList(p.WebGPUPipesAdaptor,oe);S.add(...Ie,...We,...Ve);class qe extends le{constructor(){const e={name:"webgpu",type:fe.WEBGPU,systems:ie,renderPipes:ne,renderPipeAdaptors:oe};super(e)}}export{qe as WebGPURenderer}; diff --git a/dist/assets/browserAll-BytxDfn1.js b/dist/assets/browserAll-BytxDfn1.js new file mode 100644 index 0000000..edb4889 --- /dev/null +++ b/dist/assets/browserAll-BytxDfn1.js @@ -0,0 +1,14 @@ +import{T as A,U as Z,P as g,r as te,E as b,a as ie,w as y,e as w,C as V}from"./index-DB6B0XuI.js";import"./webworkerAll-J8ccMaq4.js";import"./colorToUniform-DmtBy-2V.js";class q{constructor(e){this._lastTransform="",this._observer=null,this._tickerAttached=!1,this.updateTranslation=()=>{if(!this._canvas)return;const t=this._canvas.getBoundingClientRect(),i=this._canvas.width,n=this._canvas.height,s=t.width/i*this._renderer.resolution,o=t.height/n*this._renderer.resolution,r=t.left,l=t.top,d=`translate(${r}px, ${l}px) scale(${s}, ${o})`;d!==this._lastTransform&&(this._domElement.style.transform=d,this._lastTransform=d)},this._domElement=e.domElement,this._renderer=e.renderer,!(globalThis.OffscreenCanvas&&this._renderer.canvas instanceof OffscreenCanvas)&&(this._canvas=this._renderer.canvas,this._attachObserver())}get canvas(){return this._canvas}ensureAttached(){!this._domElement.parentNode&&this._canvas.parentNode&&(this._canvas.parentNode.appendChild(this._domElement),this.updateTranslation())}_attachObserver(){"ResizeObserver"in globalThis?(this._observer&&(this._observer.disconnect(),this._observer=null),this._observer=new ResizeObserver(e=>{for(const t of e){if(t.target!==this._canvas)continue;const i=this.canvas.width,n=this.canvas.height,s=t.contentRect.width/i*this._renderer.resolution,o=t.contentRect.height/n*this._renderer.resolution;(this._lastScaleX!==s||this._lastScaleY!==o)&&(this.updateTranslation(),this._lastScaleX=s,this._lastScaleY=o)}}),this._observer.observe(this._canvas)):this._tickerAttached||A.shared.add(this.updateTranslation,this,Z.HIGH)}destroy(){this._observer?(this._observer.disconnect(),this._observer=null):this._tickerAttached&&A.shared.remove(this.updateTranslation),this._domElement=null,this._renderer=null,this._canvas=null,this._tickerAttached=!1,this._lastTransform="",this._lastScaleX=null,this._lastScaleY=null}}class M{constructor(e){this.bubbles=!0,this.cancelBubble=!0,this.cancelable=!1,this.composed=!1,this.defaultPrevented=!1,this.eventPhase=M.prototype.NONE,this.propagationStopped=!1,this.propagationImmediatelyStopped=!1,this.layer=new g,this.page=new g,this.NONE=0,this.CAPTURING_PHASE=1,this.AT_TARGET=2,this.BUBBLING_PHASE=3,this.manager=e}get layerX(){return this.layer.x}get layerY(){return this.layer.y}get pageX(){return this.page.x}get pageY(){return this.page.y}get data(){return this}composedPath(){return this.manager&&(!this.path||this.path[this.path.length-1]!==this.target)&&(this.path=this.target?this.manager.propagationPath(this.target):[]),this.path}initEvent(e,t,i){throw new Error("initEvent() is a legacy DOM API. It is not implemented in the Federated Events API.")}initUIEvent(e,t,i,n,s){throw new Error("initUIEvent() is a legacy DOM API. It is not implemented in the Federated Events API.")}preventDefault(){this.nativeEvent instanceof Event&&this.nativeEvent.cancelable&&this.nativeEvent.preventDefault(),this.defaultPrevented=!0}stopImmediatePropagation(){this.propagationImmediatelyStopped=!0}stopPropagation(){this.propagationStopped=!0}}var I=/iPhone/i,C=/iPod/i,L=/iPad/i,U=/\biOS-universal(?:.+)Mac\b/i,k=/\bAndroid(?:.+)Mobile\b/i,R=/Android/i,E=/(?:SD4930UR|\bSilk(?:.+)Mobile\b)/i,O=/Silk/i,m=/Windows Phone/i,X=/\bWindows(?:.+)ARM\b/i,Y=/BlackBerry/i,H=/BB10/i,F=/Opera Mini/i,N=/\b(CriOS|Chrome)(?:.+)Mobile/i,$=/Mobile(?:.+)Firefox\b/i,K=function(a){return typeof a<"u"&&a.platform==="MacIntel"&&typeof a.maxTouchPoints=="number"&&a.maxTouchPoints>1&&typeof MSStream>"u"};function ne(a){return function(e){return e.test(a)}}function G(a){var e={userAgent:"",platform:"",maxTouchPoints:0};!a&&typeof navigator<"u"?e={userAgent:navigator.userAgent,platform:navigator.platform,maxTouchPoints:navigator.maxTouchPoints||0}:typeof a=="string"?e.userAgent=a:a&&a.userAgent&&(e={userAgent:a.userAgent,platform:a.platform,maxTouchPoints:a.maxTouchPoints||0});var t=e.userAgent,i=t.split("[FBAN");typeof i[1]<"u"&&(t=i[0]),i=t.split("Twitter"),typeof i[1]<"u"&&(t=i[0]);var n=ne(t),s={apple:{phone:n(I)&&!n(m),ipod:n(C),tablet:!n(I)&&(n(L)||K(e))&&!n(m),universal:n(U),device:(n(I)||n(C)||n(L)||n(U)||K(e))&&!n(m)},amazon:{phone:n(E),tablet:!n(E)&&n(O),device:n(E)||n(O)},android:{phone:!n(m)&&n(E)||!n(m)&&n(k),tablet:!n(m)&&!n(E)&&!n(k)&&(n(O)||n(R)),device:!n(m)&&(n(E)||n(O)||n(k)||n(R))||n(/\bokhttp\b/i)},windows:{phone:n(m),tablet:n(X),device:n(m)||n(X)},other:{blackberry:n(Y),blackberry10:n(H),opera:n(F),firefox:n($),chrome:n(N),device:n(Y)||n(H)||n(F)||n($)||n(N)},any:!1,phone:!1,tablet:!1};return s.any=s.apple.device||s.android.device||s.windows.device||s.other.device,s.phone=s.apple.phone||s.android.phone||s.windows.phone,s.tablet=s.apple.tablet||s.android.tablet||s.windows.tablet,s}const se=G.default??G,oe=se(globalThis.navigator),re=9,W=100,ae=0,he=0,j=2,z=1,le=-1e3,ce=-1e3,de=2,S=class J{constructor(e,t=oe){this._mobileInfo=t,this.debug=!1,this._activateOnTab=!0,this._deactivateOnMouseMove=!0,this._isActive=!1,this._isMobileAccessibility=!1,this._div=null,this._pool=[],this._renderId=0,this._children=[],this._androidUpdateCount=0,this._androidUpdateFrequency=500,this._hookDiv=null,(t.tablet||t.phone)&&this._createTouchHook(),this._renderer=e}get isActive(){return this._isActive}get isMobileAccessibility(){return this._isMobileAccessibility}get hookDiv(){return this._hookDiv}_createTouchHook(){const e=document.createElement("button");e.style.width=`${z}px`,e.style.height=`${z}px`,e.style.position="absolute",e.style.top=`${le}px`,e.style.left=`${ce}px`,e.style.zIndex=de.toString(),e.style.backgroundColor="#FF0000",e.title="select to enable accessibility for this content",e.addEventListener("focus",()=>{this._isMobileAccessibility=!0,this._activate(),this._destroyTouchHook()}),document.body.appendChild(e),this._hookDiv=e}_destroyTouchHook(){this._hookDiv&&(document.body.removeChild(this._hookDiv),this._hookDiv=null)}_activate(){if(this._isActive)return;this._isActive=!0,this._div||(this._div=document.createElement("div"),this._div.style.position="absolute",this._div.style.top=`${ae}px`,this._div.style.left=`${he}px`,this._div.style.pointerEvents="none",this._div.style.zIndex=j.toString(),this._canvasObserver=new q({domElement:this._div,renderer:this._renderer})),this._activateOnTab&&(this._onKeyDown=this._onKeyDown.bind(this),globalThis.addEventListener("keydown",this._onKeyDown,!1)),this._deactivateOnMouseMove&&(this._onMouseMove=this._onMouseMove.bind(this),globalThis.document.addEventListener("mousemove",this._onMouseMove,!0));const e=this._renderer.view.canvas;if(e.parentNode)this._canvasObserver.ensureAttached(),this._initAccessibilitySetup();else{const t=new MutationObserver(()=>{e.parentNode&&(t.disconnect(),this._canvasObserver.ensureAttached(),this._initAccessibilitySetup())});t.observe(document.body,{childList:!0,subtree:!0})}}_initAccessibilitySetup(){this._renderer.runners.postrender.add(this),this._renderer.lastObjectRendered&&this._updateAccessibleObjects(this._renderer.lastObjectRendered)}_deactivate(){if(!(!this._isActive||this._isMobileAccessibility)){this._isActive=!1,globalThis.document.removeEventListener("mousemove",this._onMouseMove,!0),this._activateOnTab&&globalThis.addEventListener("keydown",this._onKeyDown,!1),this._renderer.runners.postrender.remove(this);for(const e of this._children)e._accessibleDiv&&e._accessibleDiv.parentNode&&(e._accessibleDiv.parentNode.removeChild(e._accessibleDiv),e._accessibleDiv=null),e._accessibleActive=!1;this._pool.forEach(e=>{e.parentNode&&e.parentNode.removeChild(e)}),this._div&&this._div.parentNode&&this._div.parentNode.removeChild(this._div),this._pool=[],this._children=[]}}_updateAccessibleObjects(e){if(!e.visible||!e.accessibleChildren)return;e.accessible&&(e._accessibleActive||this._addChild(e),e._renderId=this._renderId);const t=e.children;if(t)for(let i=0;i=0;i--){const n=this._children[i];t.has(i)||(n._accessibleDiv&&n._accessibleDiv.parentNode&&(n._accessibleDiv.parentNode.removeChild(n._accessibleDiv),this._pool.push(n._accessibleDiv),n._accessibleDiv=null),n._accessibleActive=!1,te(this._children,i,1))}this._renderer.renderingToScreen&&this._canvasObserver.ensureAttached();for(let i=0;i title : ${e.title}
tabIndex: ${e.tabIndex}`}_capHitArea(e){e.x<0&&(e.width+=e.x,e.x=0),e.y<0&&(e.height+=e.y,e.y=0);const{width:t,height:i}=this._renderer;e.x+e.width>t&&(e.width=t-e.x),e.y+e.height>i&&(e.height=i-e.y)}_addChild(e){let t=this._pool.pop();t||(e.accessibleType==="button"?t=document.createElement("button"):(t=document.createElement(e.accessibleType),t.style.cssText=` + color: transparent; + pointer-events: none; + padding: 0; + margin: 0; + border: 0; + outline: 0; + background: transparent; + box-sizing: border-box; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + `,e.accessibleText&&(t.innerText=e.accessibleText)),t.style.width=`${W}px`,t.style.height=`${W}px`,t.style.backgroundColor=this.debug?"rgba(255,255,255,0.5)":"transparent",t.style.position="absolute",t.style.zIndex=j.toString(),t.style.borderStyle="none",navigator.userAgent.toLowerCase().includes("chrome")?t.setAttribute("aria-live","off"):t.setAttribute("aria-live","polite"),navigator.userAgent.match(/rv:.*Gecko\//)?t.setAttribute("aria-relevant","additions"):t.setAttribute("aria-relevant","text"),t.addEventListener("click",this._onClick.bind(this)),t.addEventListener("focus",this._onFocus.bind(this)),t.addEventListener("focusout",this._onFocusOut.bind(this))),t.style.pointerEvents=e.accessiblePointerEvents,t.type=e.accessibleType,e.accessibleTitle&&e.accessibleTitle!==null?t.title=e.accessibleTitle:(!e.accessibleHint||e.accessibleHint===null)&&(t.title=`container ${e.tabIndex}`),e.accessibleHint&&e.accessibleHint!==null&&t.setAttribute("aria-label",e.accessibleHint),e.interactive?t.tabIndex=e.tabIndex:t.tabIndex=0,this.debug&&this._updateDebugHTML(t),e._accessibleActive=!0,e._accessibleDiv=t,t.container=e,this._children.push(e),this._div.appendChild(e._accessibleDiv)}_dispatchEvent(e,t){const{container:i}=e.target,n=this._renderer.events.rootBoundary,s=Object.assign(new M(n),{target:i});n.rootTarget=this._renderer.lastObjectRendered,t.forEach(o=>n.dispatchEvent(s,o))}_onClick(e){this._dispatchEvent(e,["click","pointertap","tap"])}_onFocus(e){e.target.getAttribute("aria-live")||e.target.setAttribute("aria-live","assertive"),this._dispatchEvent(e,["mouseover"])}_onFocusOut(e){e.target.getAttribute("aria-live")||e.target.setAttribute("aria-live","polite"),this._dispatchEvent(e,["mouseout"])}_onKeyDown(e){e.keyCode!==re||!this._activateOnTab||this._activate()}_onMouseMove(e){e.movementX===0&&e.movementY===0||this._deactivate()}destroy(){var e;this._deactivate(),this._destroyTouchHook(),(e=this._canvasObserver)==null||e.destroy(),this._canvasObserver=null,this._div=null,this._pool=null,this._children=null,this._renderer=null,this._activateOnTab&&globalThis.removeEventListener("keydown",this._onKeyDown)}setAccessibilityEnabled(e){e?this._activate():this._deactivate()}};S.extension={type:[b.WebGLSystem,b.WebGPUSystem],name:"accessibility"};S.defaultOptions={enabledByDefault:!1,debug:!1,activateOnTab:!0,deactivateOnMouseMove:!0};let ue=S;const pe={accessible:!1,accessibleTitle:null,accessibleHint:null,tabIndex:0,accessibleType:"button",accessibleText:null,accessiblePointerEvents:"auto",accessibleChildren:!0,_accessibleActive:!1,_accessibleDiv:null,_renderId:-1};class Q{constructor(e){this._attachedDomElements=[],this._renderer=e,this._renderer.runners.postrender.add(this),this._renderer.runners.init.add(this),this._domElement=document.createElement("div"),this._domElement.style.position="absolute",this._domElement.style.top="0",this._domElement.style.left="0",this._domElement.style.pointerEvents="none",this._domElement.style.zIndex="1000"}init(){this._canvasObserver=new q({domElement:this._domElement,renderer:this._renderer})}addRenderable(e,t){this._attachedDomElements.includes(e)||this._attachedDomElements.push(e)}updateRenderable(e){}validateRenderable(e){return!0}postrender(){const e=this._attachedDomElements;if(e.length===0){this._domElement.remove();return}this._canvasObserver.ensureAttached();for(let t=0;ti.priority-n.priority)}dispatchEvent(e,t){e.propagationStopped=!1,e.propagationImmediatelyStopped=!1,this.propagate(e,t),this.dispatch.emit(t||e.type,e)}mapEvent(e){if(!this.rootTarget)return;const t=this.mappingTable[e.type];if(t)for(let i=0,n=t.length;i=0;n--)if(e.currentTarget=i[n],this.notifyTarget(e,t),e.propagationStopped||e.propagationImmediatelyStopped)return}}all(e,t,i=this._allInteractiveElements){if(i.length===0)return;e.eventPhase=e.BUBBLING_PHASE;const n=Array.isArray(t)?t:[t];for(let s=i.length-1;s>=0;s--)n.forEach(o=>{e.currentTarget=i[s],this.notifyTarget(e,o)})}propagationPath(e){const t=[e];for(let i=0;i=0;u--){const h=p[u],v=this.hitTestMoveRecursive(h,this._isInteractive(t)?t:h.eventMode,i,n,s,o||s(e,i));if(v){if(v.length>0&&!v[v.length-1].parent)continue;const c=e.isInteractive();(v.length>0||c)&&(c&&this._allInteractiveElements.push(e),v.push(e)),this._hitElements.length===0&&(this._hitElements=v),r=!0}}}const l=this._isInteractive(t),d=e.isInteractive();return d&&d&&this._allInteractiveElements.push(e),o||this._hitElements.length>0?null:r?this._hitElements:l&&!s(e,i)&&n(e,i)?d?[e]:[]:null}hitTestRecursive(e,t,i,n,s){if(this._interactivePrune(e)||s(e,i))return null;if((e.eventMode==="dynamic"||t==="dynamic")&&(_.pauseUpdate=!1),e.interactiveChildren&&e.children){const l=e.children,d=i;for(let p=l.length-1;p>=0;p--){const u=l[p],h=this.hitTestRecursive(u,this._isInteractive(t)?t:u.eventMode,d,n,s);if(h){if(h.length>0&&!h[h.length-1].parent)continue;const v=e.isInteractive();return(h.length>0||v)&&h.push(e),h}}}const o=this._isInteractive(t),r=e.isInteractive();return o&&n(e,i)?r?[e]:[]:null}_isInteractive(e){return e==="static"||e==="dynamic"}_interactivePrune(e){return!e||!e.visible||!e.renderable||!e.measurable||e.eventMode==="none"||e.eventMode==="passive"&&!e.interactiveChildren}hitPruneFn(e,t){if(e.hitArea&&(e.worldTransform.applyInverse(t,P),!e.hitArea.contains(P.x,P.y)))return!0;if(e.effects&&e.effects.length)for(let i=0;i0&&s!==t.target){const p=e.type==="mousemove"?"mouseout":"pointerout",u=this.createPointerEvent(e,p,s);if(this.dispatchEvent(u,"pointerout"),i&&this.dispatchEvent(u,"mouseout"),!t.composedPath().includes(s)){const h=this.createPointerEvent(e,"pointerleave",s);for(h.eventPhase=h.AT_TARGET;h.target&&!t.composedPath().includes(h.target);)h.currentTarget=h.target,this.notifyTarget(h),i&&this.notifyTarget(h,"mouseleave"),h.target=h.target.parent;this.freeEvent(h)}this.freeEvent(u)}if(s!==t.target){const p=e.type==="mousemove"?"mouseover":"pointerover",u=this.clonePointerEvent(t,p);this.dispatchEvent(u,"pointerover"),i&&this.dispatchEvent(u,"mouseover");let h=s==null?void 0:s.parent;for(;h&&h!==this.rootTarget.parent&&h!==t.target;)h=h.parent;if(!h||h===this.rootTarget.parent){const c=this.clonePointerEvent(t,"pointerenter");for(c.eventPhase=c.AT_TARGET;c.target&&c.target!==s&&c.target!==this.rootTarget.parent;)c.currentTarget=c.target,this.notifyTarget(c),i&&this.notifyTarget(c,"mouseenter"),c.target=c.target.parent;this.freeEvent(c)}this.freeEvent(u)}const o=[],r=this.enableGlobalMoveEvents??!0;this.moveOnAll?o.push("pointermove"):this.dispatchEvent(t,"pointermove"),r&&o.push("globalpointermove"),t.pointerType==="touch"&&(this.moveOnAll?o.splice(1,0,"touchmove"):this.dispatchEvent(t,"touchmove"),r&&o.push("globaltouchmove")),i&&(this.moveOnAll?o.splice(1,0,"mousemove"):this.dispatchEvent(t,"mousemove"),r&&o.push("globalmousemove"),this.cursor=(d=t.target)==null?void 0:d.cursor),o.length>0&&this.all(t,o),this._allInteractiveElements.length=0,this._hitElements.length=0,n.overTargets=t.composedPath(),this.freeEvent(t)}mapPointerOver(e){var o;if(!(e instanceof f)){y("EventBoundary cannot map a non-pointer event as a pointer event");return}const t=this.trackingData(e.pointerId),i=this.createPointerEvent(e),n=i.pointerType==="mouse"||i.pointerType==="pen";this.dispatchEvent(i,"pointerover"),n&&this.dispatchEvent(i,"mouseover"),i.pointerType==="mouse"&&(this.cursor=(o=i.target)==null?void 0:o.cursor);const s=this.clonePointerEvent(i,"pointerenter");for(s.eventPhase=s.AT_TARGET;s.target&&s.target!==this.rootTarget.parent;)s.currentTarget=s.target,this.notifyTarget(s),n&&this.notifyTarget(s,"mouseenter"),s.target=s.target.parent;t.overTargets=i.composedPath(),this.freeEvent(i),this.freeEvent(s)}mapPointerOut(e){if(!(e instanceof f)){y("EventBoundary cannot map a non-pointer event as a pointer event");return}const t=this.trackingData(e.pointerId);if(t.overTargets){const i=e.pointerType==="mouse"||e.pointerType==="pen",n=this.findMountedTarget(t.overTargets),s=this.createPointerEvent(e,"pointerout",n);this.dispatchEvent(s),i&&this.dispatchEvent(s,"mouseout");const o=this.createPointerEvent(e,"pointerleave",n);for(o.eventPhase=o.AT_TARGET;o.target&&o.target!==this.rootTarget.parent;)o.currentTarget=o.target,this.notifyTarget(o),i&&this.notifyTarget(o,"mouseleave"),o.target=o.target.parent;t.overTargets=null,this.freeEvent(s),this.freeEvent(o)}this.cursor=null}mapPointerUp(e){if(!(e instanceof f)){y("EventBoundary cannot map a non-pointer event as a pointer event");return}const t=performance.now(),i=this.createPointerEvent(e);if(this.dispatchEvent(i,"pointerup"),i.pointerType==="touch")this.dispatchEvent(i,"touchend");else if(i.pointerType==="mouse"||i.pointerType==="pen"){const r=i.button===2;this.dispatchEvent(i,r?"rightup":"mouseup")}const n=this.trackingData(e.pointerId),s=this.findMountedTarget(n.pressTargetsByButton[e.button]);let o=s;if(s&&!i.composedPath().includes(s)){let r=s;for(;r&&!i.composedPath().includes(r);){if(i.currentTarget=r,this.notifyTarget(i,"pointerupoutside"),i.pointerType==="touch")this.notifyTarget(i,"touchendoutside");else if(i.pointerType==="mouse"||i.pointerType==="pen"){const l=i.button===2;this.notifyTarget(i,l?"rightupoutside":"mouseupoutside")}r=r.parent}delete n.pressTargetsByButton[e.button],o=r}if(o){const r=this.clonePointerEvent(i,"click");r.target=o,r.path=null,n.clicksByButton[e.button]||(n.clicksByButton[e.button]={clickCount:0,target:r.target,timeStamp:t});const l=n.clicksByButton[e.button];if(l.target===r.target&&t-l.timeStamp<200?++l.clickCount:l.clickCount=1,l.target=r.target,l.timeStamp=t,r.detail=l.clickCount,r.pointerType==="mouse"){const d=r.button===2;this.dispatchEvent(r,d?"rightclick":"click")}else r.pointerType==="touch"&&this.dispatchEvent(r,"tap");this.dispatchEvent(r,"pointertap"),this.freeEvent(r)}this.freeEvent(i)}mapPointerUpOutside(e){if(!(e instanceof f)){y("EventBoundary cannot map a non-pointer event as a pointer event");return}const t=this.trackingData(e.pointerId),i=this.findMountedTarget(t.pressTargetsByButton[e.button]),n=this.createPointerEvent(e);if(i){let s=i;for(;s;)n.currentTarget=s,this.notifyTarget(n,"pointerupoutside"),n.pointerType==="touch"?this.notifyTarget(n,"touchendoutside"):(n.pointerType==="mouse"||n.pointerType==="pen")&&this.notifyTarget(n,n.button===2?"rightupoutside":"mouseupoutside"),s=s.parent;delete t.pressTargetsByButton[e.button]}this.freeEvent(n)}mapWheel(e){if(!(e instanceof T)){y("EventBoundary cannot map a non-wheel event as a wheel event");return}const t=this.createWheelEvent(e);this.dispatchEvent(t),this.freeEvent(t)}findMountedTarget(e){if(!e)return null;let t=e[0];for(let i=1;i(i==="globalMove"&&(this.rootBoundary.enableGlobalMoveEvents=n),t[i]=n,!0)}),this._onPointerDown=this._onPointerDown.bind(this),this._onPointerMove=this._onPointerMove.bind(this),this._onPointerUp=this._onPointerUp.bind(this),this._onPointerOverOut=this._onPointerOverOut.bind(this),this.onWheel=this.onWheel.bind(this)}static get defaultEventMode(){return this._defaultEventMode}init(e){const{canvas:t,resolution:i}=this.renderer;this.setTargetElement(t),this.resolution=i,x._defaultEventMode=e.eventMode??"passive",Object.assign(this.features,e.eventFeatures??{}),this.rootBoundary.enableGlobalMoveEvents=this.features.globalMove}resolutionChange(e){this.resolution=e}destroy(){_.destroy(),this.setTargetElement(null),this.renderer=null,this._currentCursor=null}setCursor(e){e||(e="default");let t=!0;if(globalThis.OffscreenCanvas&&this.domElement instanceof OffscreenCanvas&&(t=!1),this._currentCursor===e)return;this._currentCursor=e;const i=this.cursorStyles[e];if(i)switch(typeof i){case"string":t&&(this.domElement.style.cursor=i);break;case"function":i(e);break;case"object":t&&Object.assign(this.domElement.style,i);break}else t&&typeof e=="string"&&!Object.prototype.hasOwnProperty.call(this.cursorStyles,e)&&(this.domElement.style.cursor=e)}get pointer(){return this._rootPointerEvent}_onPointerDown(e){if(!this.features.click)return;this.rootBoundary.rootTarget=this.renderer.lastObjectRendered;const t=this._normalizeToPointerData(e);this.autoPreventDefault&&t[0].isNormalized&&(e.cancelable||!("cancelable"in e))&&e.preventDefault();for(let i=0,n=t.length;i0&&(t=e.composedPath()[0]);const i=t!==this.domElement?"outside":"",n=this._normalizeToPointerData(e);for(let s=0,o=n.length;s"u"&&(s.button=0),typeof s.buttons>"u"&&(s.buttons=1),typeof s.isPrimary>"u"&&(s.isPrimary=e.touches.length===1&&e.type==="touchstart"),typeof s.width>"u"&&(s.width=s.radiusX||1),typeof s.height>"u"&&(s.height=s.radiusY||1),typeof s.tiltX>"u"&&(s.tiltX=0),typeof s.tiltY>"u"&&(s.tiltY=0),typeof s.pointerType>"u"&&(s.pointerType="touch"),typeof s.pointerId>"u"&&(s.pointerId=s.identifier||0),typeof s.pressure>"u"&&(s.pressure=s.force||.5),typeof s.twist>"u"&&(s.twist=0),typeof s.tangentialPressure>"u"&&(s.tangentialPressure=0),typeof s.layerX>"u"&&(s.layerX=s.offsetX=s.clientX),typeof s.layerY>"u"&&(s.layerY=s.offsetY=s.clientY),s.isNormalized=!0,s.type=e.type,t.push(s)}else if(!globalThis.MouseEvent||e instanceof MouseEvent&&(!this.supportsPointerEvents||!(e instanceof globalThis.PointerEvent))){const i=e;typeof i.isPrimary>"u"&&(i.isPrimary=!0),typeof i.width>"u"&&(i.width=1),typeof i.height>"u"&&(i.height=1),typeof i.tiltX>"u"&&(i.tiltX=0),typeof i.tiltY>"u"&&(i.tiltY=0),typeof i.pointerType>"u"&&(i.pointerType="mouse"),typeof i.pointerId>"u"&&(i.pointerId=ge),typeof i.pressure>"u"&&(i.pressure=.5),typeof i.twist>"u"&&(i.twist=0),typeof i.tangentialPressure>"u"&&(i.tangentialPressure=0),i.isNormalized=!0,t.push(i)}else t.push(e);return t}normalizeWheelEvent(e){const t=this._rootWheelEvent;return this._transferMouseData(t,e),t.deltaX=e.deltaX,t.deltaY=e.deltaY,t.deltaZ=e.deltaZ,t.deltaMode=e.deltaMode,this.mapPositionToPoint(t.screen,e.clientX,e.clientY),t.global.copyFrom(t.screen),t.offset.copyFrom(t.screen),t.nativeEvent=e,t.type=e.type,t}_bootstrapEvent(e,t){return e.originalEvent=null,e.nativeEvent=t,e.pointerId=t.pointerId,e.width=t.width,e.height=t.height,e.isPrimary=t.isPrimary,e.pointerType=t.pointerType,e.pressure=t.pressure,e.tangentialPressure=t.tangentialPressure,e.tiltX=t.tiltX,e.tiltY=t.tiltY,e.twist=t.twist,this._transferMouseData(e,t),this.mapPositionToPoint(e.screen,t.clientX,t.clientY),e.global.copyFrom(e.screen),e.offset.copyFrom(e.screen),e.isTrusted=t.isTrusted,e.type==="pointerleave"&&(e.type="pointerout"),e.type.startsWith("mouse")&&(e.type=e.type.replace("mouse","pointer")),e.type.startsWith("touch")&&(e.type=ye[e.type]||e.type),e}_transferMouseData(e,t){e.isTrusted=t.isTrusted,e.srcElement=t.srcElement,e.timeStamp=performance.now(),e.type=t.type,e.altKey=t.altKey,e.button=t.button,e.buttons=t.buttons,e.client.x=t.clientX,e.client.y=t.clientY,e.ctrlKey=t.ctrlKey,e.metaKey=t.metaKey,e.movement.x=t.movementX,e.movement.y=t.movementY,e.page.x=t.pageX,e.page.y=t.pageY,e.relatedTarget=null,e.shiftKey=t.shiftKey}};B.extension={name:"events",type:[b.WebGLSystem,b.CanvasSystem,b.WebGPUSystem],priority:-1};B.defaultEventFeatures={move:!0,globalMove:!0,click:!0,wheel:!0};let ee=B;const be={onclick:null,onmousedown:null,onmouseenter:null,onmouseleave:null,onmousemove:null,onglobalmousemove:null,onmouseout:null,onmouseover:null,onmouseup:null,onmouseupoutside:null,onpointercancel:null,onpointerdown:null,onpointerenter:null,onpointerleave:null,onpointermove:null,onglobalpointermove:null,onpointerout:null,onpointerover:null,onpointertap:null,onpointerup:null,onpointerupoutside:null,onrightclick:null,onrightdown:null,onrightup:null,onrightupoutside:null,ontap:null,ontouchcancel:null,ontouchend:null,ontouchendoutside:null,ontouchmove:null,onglobaltouchmove:null,ontouchstart:null,onwheel:null,get interactive(){return this.eventMode==="dynamic"||this.eventMode==="static"},set interactive(a){this.eventMode=a?"static":"passive"},_internalEventMode:void 0,get eventMode(){return this._internalEventMode??ee.defaultEventMode},set eventMode(a){this._internalEventMode=a},isInteractive(){return this.eventMode==="static"||this.eventMode==="dynamic"},interactiveChildren:!0,hitArea:null,addEventListener(a,e,t){const i=typeof t=="boolean"&&t||typeof t=="object"&&t.capture,n=typeof t=="object"?t.signal:void 0,s=typeof t=="object"?t.once===!0:!1,o=typeof e=="function"?void 0:e;a=i?`${a}capture`:a;const r=typeof e=="function"?e:e.handleEvent,l=this;n&&n.addEventListener("abort",()=>{l.off(a,r,o)}),s?l.once(a,r,o):l.on(a,r,o)},removeEventListener(a,e,t){const i=typeof t=="boolean"&&t||typeof t=="object"&&t.capture,n=typeof e=="function"?void 0:e;a=i?`${a}capture`:a,e=typeof e=="function"?e:e.handleEvent,this.off(a,e,n)},dispatchEvent(a){if(!(a instanceof M))throw new Error("Container cannot propagate events outside of the Federated Events API");return a.defaultPrevented=!1,a.path=null,a.target=this,a.manager.dispatchEvent(a),!a.defaultPrevented}};w.add(ue);w.mixin(V,pe);w.add(ee);w.mixin(V,be);w.add(Q); diff --git a/dist/assets/colorToUniform-DmtBy-2V.js b/dist/assets/colorToUniform-DmtBy-2V.js new file mode 100644 index 0000000..2c8be17 --- /dev/null +++ b/dist/assets/colorToUniform-DmtBy-2V.js @@ -0,0 +1,31 @@ +const f={normal:0,add:1,multiply:2,screen:3,overlay:4,erase:5,"normal-npm":6,"add-npm":7,"screen-npm":8,min:9,max:10},n=0,i=1,r=2,a=3,l=4,d=5,h=class u{constructor(){this.data=0,this.blendMode="normal",this.polygonOffset=0,this.blend=!0,this.depthMask=!0}get blend(){return!!(this.data&1<, + uColor:vec4, + uRound:f32, + } + + @group(1) @binding(0) var localUniforms : LocalUniforms; + `,main:` + vColor *= localUniforms.uColor; + modelMatrix *= localUniforms.uTransformMatrix; + `,end:` + if(localUniforms.uRound == 1) + { + vPosition = vec4(roundPixels(vPosition.xy, globalUniforms.uResolution), vPosition.zw); + } + `}},g={...c,vertex:{...c.vertex,header:c.vertex.header.replace("group(1)","group(2)")}},p={name:"local-uniform-bit",vertex:{header:` + + uniform mat3 uTransformMatrix; + uniform vec4 uColor; + uniform float uRound; + `,main:` + vColor *= uColor; + modelMatrix = uTransformMatrix; + `,end:` + if(uRound == 1.) + { + gl_Position.xy = roundPixels(gl_Position.xy, uResolution); + } + `}};class b{constructor(){this.batcherName="default",this.topology="triangle-list",this.attributeSize=4,this.indexSize=6,this.packAsQuad=!0,this.roundPixels=0,this._attributeStart=0,this._batcher=null,this._batch=null}get blendMode(){return this.renderable.groupBlendMode}get color(){return this.renderable.groupColorAlpha}reset(){this.renderable=null,this.texture=null,this._batcher=null,this._batch=null,this.bounds=null}destroy(){}}function M(e,t,o){const s=(e>>24&255)/255;t[o++]=(e&255)/255*s,t[o++]=(e>>8&255)/255*s,t[o++]=(e>>16&255)/255*s,t[o++]=s}export{b as B,m as S,c as a,p as b,M as c,g as l}; diff --git a/dist/assets/index-DB6B0XuI.js b/dist/assets/index-DB6B0XuI.js new file mode 100644 index 0000000..09ed45c --- /dev/null +++ b/dist/assets/index-DB6B0XuI.js @@ -0,0 +1,451 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/browserAll-BytxDfn1.js","assets/webworkerAll-J8ccMaq4.js","assets/colorToUniform-DmtBy-2V.js","assets/WebGPURenderer-CtyVoQqf.js","assets/SharedSystems-DVK37F7d.js","assets/WebGLRenderer-BVO0qznQ.js"])))=>i.map(i=>d[i]); +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))i(r);new MutationObserver(r=>{for(const n of r)if(n.type==="childList")for(const o of n.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function e(r){const n={};return r.integrity&&(n.integrity=r.integrity),r.referrerPolicy&&(n.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?n.credentials="include":r.crossOrigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function i(r){if(r.ep)return;r.ep=!0;const n=e(r);fetch(r.href,n)}})();class Wn{constructor(){this.listeners=new Map}on(t,e){this.listeners.has(t)||this.listeners.set(t,new Set);const i=this.listeners.get(t);return i.add(e),()=>{i.delete(e),i.size===0&&this.listeners.delete(t)}}emit(t,e){const i=this.listeners.get(t);if(i)for(const r of i)r(e)}}const _r="timeplot.app-state.v1";function Ns(s){return Object.fromEntries(Object.entries(s).map(([t,e])=>[t,{...e}]))}function Us(s){return Object.fromEntries(Object.entries(s).map(([t,e])=>[t,{...e}]))}function Yn(s){return{type:s.type,preset:s.preset,sampleRateHz:s.sampleRateHz,amplitude:s.amplitude,noise:s.noise,replayRate:s.replayRate,dataFileName:s.dataFileName,wsUrl:s.wsUrl,wsReconnectMs:s.wsReconnectMs}}function Xn(s){return{plot:{showGrid:s.plot.showGrid,showPoints:s.plot.showPoints,windowDurationMs:s.plot.windowDurationMs,maxPoints:s.plot.maxPoints},time:{speed:s.time.speed},panels:Ns(s.panels),graphs:Us(s.graphs),sources:Object.fromEntries(Object.entries(s.sources).map(([t,e])=>[t,Yn(e)]))}}function jn(s,t){return!t||typeof t!="object"?s:{...s,time:t.time?{...s.time,speed:t.time.speed??s.time.speed,paused:!1}:s.time,plot:t.plot?{...s.plot,...t.plot,valueRange:s.plot.valueRange,hoveredPoint:null,tooltip:{...s.plot.tooltip}}:s.plot,panels:t.panels?Ns(Object.fromEntries(Object.entries(s.panels).map(([i,r])=>[i,{...r,...t.panels[i]??{}}]))):s.panels,graphs:t.graphs?Us(Object.fromEntries(Object.entries(s.graphs).map(([i,r])=>[i,{...r,...t.graphs[i]??{}}]))):s.graphs,sources:t.sources?Object.fromEntries(Object.entries(s.sources).map(([i,r])=>{const n=t.sources[i]??{},o=n.type??r.type;return[i,{...r,...n,type:o,dataset:[],datasetPointCount:0,datasetDurationMs:0,loadError:o==="csv-replay"&&n.dataFileName?`Reload ${n.dataFileName} to restore replay data`:"",wsStatus:"idle",wsStatusDetail:""}]})):s.sources}}function qn(){if(typeof localStorage>"u")return null;try{const s=localStorage.getItem(_r);return s?JSON.parse(s):null}catch(s){return console.warn("[timeplot] failed to load persisted state",s),null}}function Zn(s){if(!(typeof localStorage>"u"))try{localStorage.setItem(_r,JSON.stringify(Xn(s)))}catch(t){console.warn("[timeplot] failed to persist state",t)}}function br(){return{app:{title:"TimePlot",renderer:"pending"},time:{realNowMs:Date.now(),realElapsedMs:0,plotTimeMs:0,speed:1,paused:!1},plot:{showGrid:!0,showPoints:!0,windowDurationMs:2e4,maxPoints:1600,valueRange:{min:-1.6,max:1.6},hoveredPoint:null,tooltip:{visible:!1,x:0,y:0,point:null}},sources:{signalA:{id:"signal-a",label:"Signal A",type:"synthetic-wave",preset:"telemetry",sampleRateHz:60,amplitude:1,noise:.08,replayRate:1,dataset:[],dataFileName:"",datasetPointCount:0,datasetDurationMs:0,loadError:"",wsUrl:"ws://localhost:8080",wsReconnectMs:2e3,wsStatus:"idle",wsStatusDetail:""},signalB:{id:"signal-b",label:"Signal B",type:"synthetic-wave",preset:"chirp",sampleRateHz:48,amplitude:.8,noise:.04,replayRate:1,dataset:[],dataFileName:"",datasetPointCount:0,datasetDurationMs:0,loadError:"",wsUrl:"ws://localhost:8080",wsReconnectMs:2e3,wsStatus:"idle",wsStatusDetail:""}},graphs:{primary:{sourceKey:"signalA",transform:"raw",title:"Primary signal"},secondary:{sourceKey:"signalB",transform:"delta",title:"Secondary signal"}},panels:{status:{title:"Status",visible:!0},source:{title:"Data Source",visible:!0},config:{title:"Config",visible:!0},help:{title:"Help",visible:!1}}}}class Kn{constructor(t=br()){this.state=jn(t,qn()),this.listeners=new Set}getState(){return this.state}subscribe(t){return this.listeners.add(t),()=>this.listeners.delete(t)}setState(t){const e=typeof t=="function"?t(this.state):t;this.state=e,Zn(this.state);for(const i of this.listeners)i(this.state)}patch(t){this.setState(e=>({...e,...t,time:t.time?{...e.time,...t.time}:e.time,plot:t.plot?{...e.plot,...t.plot,valueRange:t.plot.valueRange?{...e.plot.valueRange,...t.plot.valueRange}:e.plot.valueRange,tooltip:t.plot.tooltip?{...e.plot.tooltip,...t.plot.tooltip}:e.plot.tooltip}:e.plot,sources:t.sources?Object.fromEntries(Object.entries({...e.sources,...t.sources}).map(([i,r])=>[i,{...e.sources[i],...r}])):e.sources,graphs:t.graphs?Us(Object.fromEntries(Object.entries({...e.graphs,...t.graphs}).map(([i,r])=>[i,{...e.graphs[i],...r}]))):e.graphs,panels:t.panels?Ns({...e.panels,...t.panels}):e.panels}))}}class Qn{constructor(t){this.store=t,this.lastFrameTime=performance.now()}tick(t=performance.now()){const e=t-this.lastFrameTime;return this.lastFrameTime=t,this.store.setState(i=>{const r=i.time.realElapsedMs+e,n=i.time.paused?0:e*i.time.speed;return{...i,time:{...i.time,realNowMs:Date.now(),realElapsedMs:r,plotTimeMs:Math.max(0,i.time.plotTimeMs+n)}}}),e}togglePause(){this.store.setState(t=>({...t,time:{...t.time,paused:!t.time.paused}}))}setPaused(t){this.store.setState(e=>({...e,time:{...e.time,paused:t}}))}setSpeed(t){const e=Math.max(.1,Math.min(12,t));this.store.setState(i=>({...i,time:{...i.time,speed:e}}))}reset(){this.store.setState(t=>({...t,time:{...t.time,realElapsedMs:0,plotTimeMs:0},plot:{...t.plot,hoveredPoint:null,tooltip:{...t.plot.tooltip,visible:!1,point:null}}})),this.lastFrameTime=performance.now()}}class ni{constructor(t=1600){this.maxPoints=t,this.points=[]}addPoint(t){this.points.push(t),this.points.length>this.maxPoints&&this.points.splice(0,this.points.length-this.maxPoints)}clear(){this.points=[]}getVisiblePoints(t,e){const i=t-e;return this.points.filter(r=>r.timeMs>=i&&r.timeMs<=t)}}const Jn="modulepreload",to=function(s){return"/"+s},oi={},Ne=function(t,e,i){let r=Promise.resolve();if(e&&e.length>0){document.getElementsByTagName("link");const o=document.querySelector("meta[property=csp-nonce]"),a=(o==null?void 0:o.nonce)||(o==null?void 0:o.getAttribute("nonce"));r=Promise.allSettled(e.map(h=>{if(h=to(h),h in oi)return;oi[h]=!0;const l=h.endsWith(".css"),c=l?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${h}"]${c}`))return;const u=document.createElement("link");if(u.rel=l?"stylesheet":Jn,l||(u.as="script"),u.crossOrigin="",u.href=h,a&&u.setAttribute("nonce",a),document.head.appendChild(u),l)return new Promise((p,d)=>{u.addEventListener("load",p),u.addEventListener("error",()=>d(new Error(`Unable to preload CSS for ${h}`)))})}))}function n(o){const a=new Event("vite:preloadError",{cancelable:!0});if(a.payload=o,window.dispatchEvent(a),!a.defaultPrevented)throw o}return r.then(o=>{for(const a of o||[])a.status==="rejected"&&n(a.reason);return t().catch(n)})};var L=(s=>(s.Application="application",s.WebGLPipes="webgl-pipes",s.WebGLPipesAdaptor="webgl-pipes-adaptor",s.WebGLSystem="webgl-system",s.WebGPUPipes="webgpu-pipes",s.WebGPUPipesAdaptor="webgpu-pipes-adaptor",s.WebGPUSystem="webgpu-system",s.CanvasSystem="canvas-system",s.CanvasPipesAdaptor="canvas-pipes-adaptor",s.CanvasPipes="canvas-pipes",s.Asset="asset",s.LoadParser="load-parser",s.ResolveParser="resolve-parser",s.CacheParser="cache-parser",s.DetectionParser="detection-parser",s.MaskEffect="mask-effect",s.BlendMode="blend-mode",s.TextureSource="texture-source",s.Environment="environment",s.ShapeBuilder="shape-builder",s.Batcher="batcher",s))(L||{});const ys=s=>{if(typeof s=="function"||typeof s=="object"&&s.extension){if(!s.extension)throw new Error("Extension class must have an extension object");s={...typeof s.extension!="object"?{type:s.extension}:s.extension,ref:s}}if(typeof s=="object")s={...s};else throw new Error("Invalid extension type");return typeof s.type=="string"&&(s.type=[s.type]),s},ve=(s,t)=>ys(s).priority??t,rt={_addHandlers:{},_removeHandlers:{},_queue:{},remove(...s){return s.map(ys).forEach(t=>{t.type.forEach(e=>{var i,r;return(r=(i=this._removeHandlers)[e])==null?void 0:r.call(i,t)})}),this},add(...s){return s.map(ys).forEach(t=>{t.type.forEach(e=>{var n,o;const i=this._addHandlers,r=this._queue;i[e]?(o=i[e])==null||o.call(i,t):(r[e]=r[e]||[],(n=r[e])==null||n.push(t))})}),this},handle(s,t,e){var o;const i=this._addHandlers,r=this._removeHandlers;if(i[s]||r[s])throw new Error(`Extension type ${s} already has a handler`);i[s]=t,r[s]=e;const n=this._queue;return n[s]&&((o=n[s])==null||o.forEach(a=>t(a)),delete n[s]),this},handleByMap(s,t){return this.handle(s,e=>{e.name&&(t[e.name]=e.ref)},e=>{e.name&&delete t[e.name]})},handleByNamedList(s,t,e=-1){return this.handle(s,i=>{t.findIndex(n=>n.name===i.name)>=0||(t.push({name:i.name,value:i.ref}),t.sort((n,o)=>ve(o.value,e)-ve(n.value,e)))},i=>{const r=t.findIndex(n=>n.name===i.name);r!==-1&&t.splice(r,1)})},handleByList(s,t,e=-1){return this.handle(s,i=>{t.includes(i.ref)||(t.push(i.ref),t.sort((r,n)=>ve(n,e)-ve(r,e)))},i=>{const r=t.indexOf(i.ref);r!==-1&&t.splice(r,1)})},mixin(s,...t){for(const e of t)Object.defineProperties(s.prototype,Object.getOwnPropertyDescriptors(e))}},eo={extension:{type:L.Environment,name:"browser",priority:-1},test:()=>!0,load:async()=>{await Ne(()=>import("./browserAll-BytxDfn1.js"),__vite__mapDeps([0,1,2]))}},so={extension:{type:L.Environment,name:"webworker",priority:0},test:()=>typeof self<"u"&&self.WorkerGlobalScope!==void 0,load:async()=>{await Ne(()=>import("./webworkerAll-J8ccMaq4.js"),__vite__mapDeps([1,2]))}};class q{constructor(t,e,i){this._x=e||0,this._y=i||0,this._observer=t}clone(t){return new q(t??this._observer,this._x,this._y)}set(t=0,e=t){return(this._x!==t||this._y!==e)&&(this._x=t,this._y=e,this._observer._onUpdate(this)),this}copyFrom(t){return(this._x!==t.x||this._y!==t.y)&&(this._x=t.x,this._y=t.y,this._observer._onUpdate(this)),this}copyTo(t){return t.set(this._x,this._y),t}equals(t){return t.x===this._x&&t.y===this._y}toString(){return`[pixi.js/math:ObservablePoint x=${this._x} y=${this._y} scope=${this._observer}]`}get x(){return this._x}set x(t){this._x!==t&&(this._x=t,this._observer._onUpdate(this))}get y(){return this._y}set y(t){this._y!==t&&(this._y=t,this._observer._onUpdate(this))}}function wr(s){return s&&s.__esModule&&Object.prototype.hasOwnProperty.call(s,"default")?s.default:s}var Sr={exports:{}};(function(s){var t=Object.prototype.hasOwnProperty,e="~";function i(){}Object.create&&(i.prototype=Object.create(null),new i().__proto__||(e=!1));function r(h,l,c){this.fn=h,this.context=l,this.once=c||!1}function n(h,l,c,u,p){if(typeof c!="function")throw new TypeError("The listener must be a function");var d=new r(c,u||h,p),m=e?e+l:l;return h._events[m]?h._events[m].fn?h._events[m]=[h._events[m],d]:h._events[m].push(d):(h._events[m]=d,h._eventsCount++),h}function o(h,l){--h._eventsCount===0?h._events=new i:delete h._events[l]}function a(){this._events=new i,this._eventsCount=0}a.prototype.eventNames=function(){var l=[],c,u;if(this._eventsCount===0)return l;for(u in c=this._events)t.call(c,u)&&l.push(e?u.slice(1):u);return Object.getOwnPropertySymbols?l.concat(Object.getOwnPropertySymbols(c)):l},a.prototype.listeners=function(l){var c=e?e+l:l,u=this._events[c];if(!u)return[];if(u.fn)return[u.fn];for(var p=0,d=u.length,m=new Array(d);pkt[s],uY:s=>Et[s],vX:s=>Rt[s],vY:s=>It[s],inv:s=>s&8?s&15:-s&7,add:(s,t)=>_s[s][t],sub:(s,t)=>_s[s][z.inv(t)],rotate180:s=>s^4,isVertical:s=>(s&3)===2,byDirection:(s,t)=>Math.abs(s)*2<=Math.abs(t)?t>=0?z.S:z.N:Math.abs(t)*2<=Math.abs(s)?s>0?z.E:z.W:t>0?s>0?z.SE:z.SW:s>0?z.NE:z.NW,matrixAppendRotationInv:(s,t,e=0,i=0)=>{const r=vr[z.inv(t)];r.tx=e,r.ty=i,s.append(r)},transformRectCoords:(s,t,e,i)=>{const{x:r,y:n,width:o,height:a}=s,{x:h,y:l,width:c,height:u}=t;return e===z.E?(i.set(r+h,n+l,o,a),i):e===z.S?i.set(c-n-a+h,r+l,a,o):e===z.W?i.set(c-r-o+h,u-n-a+l,o,a):e===z.N?i.set(n+h,u-r-o+l,a,o):i.set(r+h,n+l,o,a)}},Me=[new Z,new Z,new Z,new Z];class W{constructor(t=0,e=0,i=0,r=0){this.type="rectangle",this.x=Number(t),this.y=Number(e),this.width=Number(i),this.height=Number(r)}get left(){return this.x}get right(){return this.x+this.width}get top(){return this.y}get bottom(){return this.y+this.height}isEmpty(){return this.left===this.right||this.top===this.bottom}static get EMPTY(){return new W(0,0,0,0)}clone(){return new W(this.x,this.y,this.width,this.height)}copyFromBounds(t){return this.x=t.minX,this.y=t.minY,this.width=t.maxX-t.minX,this.height=t.maxY-t.minY,this}copyFrom(t){return this.x=t.x,this.y=t.y,this.width=t.width,this.height=t.height,this}copyTo(t){return t.copyFrom(this),t}contains(t,e){return this.width<=0||this.height<=0?!1:t>=this.x&&t=this.y&&e=u&&t<=p&&e>=d&&e<=m&&!(t>g&&tx&&et.right?t.right:this.right)<=R)return!1;const P=this.yt.bottom?t.bottom:this.bottom)>P}const i=this.left,r=this.right,n=this.top,o=this.bottom;if(r<=i||o<=n)return!1;const a=Me[0].set(t.left,t.top),h=Me[1].set(t.left,t.bottom),l=Me[2].set(t.right,t.top),c=Me[3].set(t.right,t.bottom);if(l.x<=a.x||h.y<=a.y)return!1;const u=Math.sign(e.a*e.d-e.b*e.c);if(u===0||(e.apply(a,a),e.apply(h,h),e.apply(l,l),e.apply(c,c),Math.max(a.x,h.x,l.x,c.x)<=i||Math.min(a.x,h.x,l.x,c.x)>=r||Math.max(a.y,h.y,l.y,c.y)<=n||Math.min(a.y,h.y,l.y,c.y)>=o))return!1;const p=u*(h.y-a.y),d=u*(a.x-h.x),m=p*i+d*n,g=p*r+d*n,f=p*i+d*o,x=p*r+d*o;if(Math.max(m,g,f,x)<=p*a.x+d*a.y||Math.min(m,g,f,x)>=p*c.x+d*c.y)return!1;const y=u*(a.y-l.y),_=u*(l.x-a.x),b=y*i+_*n,v=y*r+_*n,S=y*i+_*o,w=y*r+_*o;return!(Math.max(b,v,S,w)<=y*a.x+_*a.y||Math.min(b,v,S,w)>=y*c.x+_*c.y)}pad(t=0,e=t){return this.x-=t,this.y-=e,this.width+=t*2,this.height+=e*2,this}fit(t){const e=Math.max(this.x,t.x),i=Math.min(this.x+this.width,t.x+t.width),r=Math.max(this.y,t.y),n=Math.min(this.y+this.height,t.y+t.height);return this.x=e,this.width=Math.max(i-e,0),this.y=r,this.height=Math.max(n-r,0),this}ceil(t=1,e=.001){const i=Math.ceil((this.x+this.width-e)*t)/t,r=Math.ceil((this.y+this.height-e)*t)/t;return this.x=Math.floor((this.x+e)*t)/t,this.y=Math.floor((this.y+e)*t)/t,this.width=i-this.x,this.height=r-this.y,this}scale(t,e=t){return this.x*=t,this.y*=e,this.width*=t,this.height*=e,this}enlarge(t){const e=Math.min(this.x,t.x),i=Math.max(this.x+this.width,t.x+t.width),r=Math.min(this.y,t.y),n=Math.max(this.y+this.height,t.y+t.height);return this.x=e,this.width=i-e,this.y=r,this.height=n-r,this}getBounds(t){return t||(t=new W),t.copyFrom(this),t}containsRect(t){if(this.width<=0||this.height<=0)return!1;const e=t.x,i=t.y,r=t.x+t.width,n=t.y+t.height;return e>=this.x&&e=this.y&&i=this.x&&r=this.y&&n{if($t.quiet||ai.has(t))return;let i=new Error().stack;const r=`${t} +Deprecated since v${s}`,n=typeof console.groupCollapsed=="function"&&!$t.noColor;typeof i>"u"?console.warn("PixiJS Deprecation Warning: ",r):(i=i.split(` +`).splice(e).join(` +`),n?(console.groupCollapsed("%cPixiJS Deprecation Warning: %c%s","color:#614108;background:#fffbe6","font-weight:normal;color:#614108;background:#fffbe6",r),console.warn(i),console.groupEnd()):(console.warn("PixiJS Deprecation Warning: ",r),console.warn(i))),ai.add(t)};Object.defineProperties(F,{quiet:{get:()=>$t.quiet,set:s=>{$t.quiet=s},enumerable:!0,configurable:!1},noColor:{get:()=>$t.noColor,set:s=>{$t.noColor=s},enumerable:!0,configurable:!1}});const Pr=()=>{};function Vt(s){return s+=s===0?1:0,--s,s|=s>>>1,s|=s>>>2,s|=s>>>4,s|=s>>>8,s|=s>>>16,s+1}function hi(s){return!(s&s-1)&&!!s}function Mr(s){const t={};for(const e in s)s[e]!==void 0&&(t[e]=s[e]);return t}const li=Object.create(null);function uo(s){const t=li[s];return t===void 0&&(li[s]=V("resource")),t}const Cr=class Tr extends pt{constructor(t={}){super(),this._resourceType="textureSampler",this._touched=0,this._maxAnisotropy=1,this.destroyed=!1,t={...Tr.defaultOptions,...t},this.addressMode=t.addressMode,this.addressModeU=t.addressModeU??this.addressModeU,this.addressModeV=t.addressModeV??this.addressModeV,this.addressModeW=t.addressModeW??this.addressModeW,this.scaleMode=t.scaleMode,this.magFilter=t.magFilter??this.magFilter,this.minFilter=t.minFilter??this.minFilter,this.mipmapFilter=t.mipmapFilter??this.mipmapFilter,this.lodMinClamp=t.lodMinClamp,this.lodMaxClamp=t.lodMaxClamp,this.compare=t.compare,this.maxAnisotropy=t.maxAnisotropy??1}set addressMode(t){this.addressModeU=t,this.addressModeV=t,this.addressModeW=t}get addressMode(){return this.addressModeU}set wrapMode(t){F(O,"TextureStyle.wrapMode is now TextureStyle.addressMode"),this.addressMode=t}get wrapMode(){return this.addressMode}set scaleMode(t){this.magFilter=t,this.minFilter=t,this.mipmapFilter=t}get scaleMode(){return this.magFilter}set maxAnisotropy(t){this._maxAnisotropy=Math.min(t,16),this._maxAnisotropy>1&&(this.scaleMode="linear")}get maxAnisotropy(){return this._maxAnisotropy}get _resourceId(){return this._sharedResourceId||this._generateResourceId()}update(){this.emit("change",this),this._sharedResourceId=null}_generateResourceId(){const t=`${this.addressModeU}-${this.addressModeV}-${this.addressModeW}-${this.magFilter}-${this.minFilter}-${this.mipmapFilter}-${this.lodMinClamp}-${this.lodMaxClamp}-${this.compare}-${this._maxAnisotropy}`;return this._sharedResourceId=uo(t),this._resourceId}destroy(){this.destroyed=!0,this.emit("destroy",this),this.emit("change",this),this.removeAllListeners()}};Cr.defaultOptions={addressMode:"clamp-to-edge",scaleMode:"linear"};let Ue=Cr;const Ar=class kr extends pt{constructor(t={}){super(),this.options=t,this.uid=V("textureSource"),this._resourceType="textureSource",this._resourceId=V("resource"),this.uploadMethodId="unknown",this._resolution=1,this.pixelWidth=1,this.pixelHeight=1,this.width=1,this.height=1,this.sampleCount=1,this.mipLevelCount=1,this.autoGenerateMipmaps=!1,this.format="rgba8unorm",this.dimension="2d",this.antialias=!1,this._touched=0,this._batchTick=-1,this._textureBindLocation=-1,t={...kr.defaultOptions,...t},this.label=t.label??"",this.resource=t.resource,this.autoGarbageCollect=t.autoGarbageCollect,this._resolution=t.resolution,t.width?this.pixelWidth=t.width*this._resolution:this.pixelWidth=this.resource?this.resourceWidth??1:1,t.height?this.pixelHeight=t.height*this._resolution:this.pixelHeight=this.resource?this.resourceHeight??1:1,this.width=this.pixelWidth/this._resolution,this.height=this.pixelHeight/this._resolution,this.format=t.format,this.dimension=t.dimensions,this.mipLevelCount=t.mipLevelCount,this.autoGenerateMipmaps=t.autoGenerateMipmaps,this.sampleCount=t.sampleCount,this.antialias=t.antialias,this.alphaMode=t.alphaMode,this.style=new Ue(Mr(t)),this.destroyed=!1,this._refreshPOT()}get source(){return this}get style(){return this._style}set style(t){var e,i;this.style!==t&&((e=this._style)==null||e.off("change",this._onStyleChange,this),this._style=t,(i=this._style)==null||i.on("change",this._onStyleChange,this),this._onStyleChange())}set maxAnisotropy(t){this._style.maxAnisotropy=t}get maxAnisotropy(){return this._style.maxAnisotropy}get addressMode(){return this._style.addressMode}set addressMode(t){this._style.addressMode=t}get repeatMode(){return this._style.addressMode}set repeatMode(t){this._style.addressMode=t}get magFilter(){return this._style.magFilter}set magFilter(t){this._style.magFilter=t}get minFilter(){return this._style.minFilter}set minFilter(t){this._style.minFilter=t}get mipmapFilter(){return this._style.mipmapFilter}set mipmapFilter(t){this._style.mipmapFilter=t}get lodMinClamp(){return this._style.lodMinClamp}set lodMinClamp(t){this._style.lodMinClamp=t}get lodMaxClamp(){return this._style.lodMaxClamp}set lodMaxClamp(t){this._style.lodMaxClamp=t}_onStyleChange(){this.emit("styleChange",this)}update(){if(this.resource){const t=this._resolution;if(this.resize(this.resourceWidth/t,this.resourceHeight/t))return}this.emit("update",this)}destroy(){this.destroyed=!0,this.emit("destroy",this),this.emit("change",this),this._style&&(this._style.destroy(),this._style=null),this.uploadMethodId=null,this.resource=null,this.removeAllListeners()}unload(){this._resourceId=V("resource"),this.emit("change",this),this.emit("unload",this)}get resourceWidth(){const{resource:t}=this;return t.naturalWidth||t.videoWidth||t.displayWidth||t.width}get resourceHeight(){const{resource:t}=this;return t.naturalHeight||t.videoHeight||t.displayHeight||t.height}get resolution(){return this._resolution}set resolution(t){this._resolution!==t&&(this._resolution=t,this.width=this.pixelWidth/t,this.height=this.pixelHeight/t)}resize(t,e,i){i||(i=this._resolution),t||(t=this.width),e||(e=this.height);const r=Math.round(t*i),n=Math.round(e*i);return this.width=r/i,this.height=n/i,this._resolution=i,this.pixelWidth===r&&this.pixelHeight===n?!1:(this._refreshPOT(),this.pixelWidth=r,this.pixelHeight=n,this.emit("resize",this),this._resourceId=V("resource"),this.emit("change",this),!0)}updateMipmaps(){this.autoGenerateMipmaps&&this.mipLevelCount>1&&this.emit("updateMipmaps",this)}set wrapMode(t){this._style.wrapMode=t}get wrapMode(){return this._style.wrapMode}set scaleMode(t){this._style.scaleMode=t}get scaleMode(){return this._style.scaleMode}_refreshPOT(){this.isPowerOfTwo=hi(this.pixelWidth)&&hi(this.pixelHeight)}static test(t){throw new Error("Unimplemented")}};Ar.defaultOptions={resolution:1,format:"bgra8unorm",alphaMode:"premultiply-alpha-on-upload",dimensions:"2d",mipLevelCount:1,autoGenerateMipmaps:!1,sampleCount:1,antialias:!1,autoGarbageCollect:!1};let ht=Ar;class $s extends ht{constructor(t){const e=t.resource||new Float32Array(t.width*t.height*4);let i=t.format;i||(e instanceof Float32Array?i="rgba32float":e instanceof Int32Array||e instanceof Uint32Array?i="rgba32uint":e instanceof Int16Array||e instanceof Uint16Array?i="rgba16uint":(e instanceof Int8Array,i="bgra8unorm")),super({...t,resource:e,format:i}),this.uploadMethodId="buffer"}static test(t){return t instanceof Int8Array||t instanceof Uint8Array||t instanceof Uint8ClampedArray||t instanceof Int16Array||t instanceof Uint16Array||t instanceof Int32Array||t instanceof Uint32Array||t instanceof Float32Array}}$s.extension=L.TextureSource;const ci=new I;class fo{constructor(t,e){this.mapCoord=new I,this.uClampFrame=new Float32Array(4),this.uClampOffset=new Float32Array(2),this._textureID=-1,this._updateID=0,this.clampOffset=0,typeof e>"u"?this.clampMargin=t.width<10?0:.5:this.clampMargin=e,this.isSimple=!1,this.texture=t}get texture(){return this._texture}set texture(t){var e;this.texture!==t&&((e=this._texture)==null||e.removeListener("update",this.update,this),this._texture=t,this._texture.addListener("update",this.update,this),this.update())}multiplyUvs(t,e){e===void 0&&(e=t);const i=this.mapCoord;for(let r=0;rthis.maxX||this.minY>this.maxY}get rectangle(){this._rectangle||(this._rectangle=new W);const t=this._rectangle;return this.minX>this.maxX||this.minY>this.maxY?(t.x=0,t.y=0,t.width=0,t.height=0):t.copyFromBounds(this),t}clear(){return this.minX=1/0,this.minY=1/0,this.maxX=-1/0,this.maxY=-1/0,this.matrix=ui,this}set(t,e,i,r){this.minX=t,this.minY=e,this.maxX=i,this.maxY=r}addFrame(t,e,i,r,n){n||(n=this.matrix);const o=n.a,a=n.b,h=n.c,l=n.d,c=n.tx,u=n.ty;let p=this.minX,d=this.minY,m=this.maxX,g=this.maxY,f=o*t+h*e+c,x=a*t+l*e+u;fm&&(m=f),x>g&&(g=x),f=o*i+h*e+c,x=a*i+l*e+u,fm&&(m=f),x>g&&(g=x),f=o*t+h*r+c,x=a*t+l*r+u,fm&&(m=f),x>g&&(g=x),f=o*i+h*r+c,x=a*i+l*r+u,fm&&(m=f),x>g&&(g=x),this.minX=p,this.minY=d,this.maxX=m,this.maxY=g}addRect(t,e){this.addFrame(t.x,t.y,t.x+t.width,t.y+t.height,e)}addBounds(t,e){this.addFrame(t.minX,t.minY,t.maxX,t.maxY,e)}addBoundsMask(t){this.minX=this.minX>t.minX?this.minX:t.minX,this.minY=this.minY>t.minY?this.minY:t.minY,this.maxX=this.maxXthis.maxX?p:this.maxX,this.maxY=d>this.maxY?d:this.maxY,p=o*e+h*n+c,d=a*e+l*n+u,this.minX=pthis.maxX?p:this.maxX,this.maxY=d>this.maxY?d:this.maxY,p=o*r+h*n+c,d=a*r+l*n+u,this.minX=pthis.maxX?p:this.maxX,this.maxY=d>this.maxY?d:this.maxY}fit(t){return this.minXt.right&&(this.maxX=t.right),this.minYt.bottom&&(this.maxY=t.bottom),this}fitBounds(t,e,i,r){return this.minXe&&(this.maxX=e),this.minYr&&(this.maxY=r),this}pad(t,e=t){return this.minX-=t,this.maxX+=t,this.minY-=e,this.maxY+=e,this}ceil(){return this.minX=Math.floor(this.minX),this.minY=Math.floor(this.minY),this.maxX=Math.ceil(this.maxX),this.maxY=Math.ceil(this.maxY),this}clone(){return new at(this.minX,this.minY,this.maxX,this.maxY)}scale(t,e=t){return this.minX*=t,this.minY*=e,this.maxX*=t,this.maxY*=e,this}get x(){return this.minX}set x(t){const e=this.maxX-this.minX;this.minX=t,this.maxX=t+e}get y(){return this.minY}set y(t){const e=this.maxY-this.minY;this.minY=t,this.maxY=t+e}get width(){return this.maxX-this.minX}set width(t){this.maxX=this.minX+t}get height(){return this.maxY-this.minY}set height(t){this.maxY=this.minY+t}get left(){return this.minX}get right(){return this.maxX}get top(){return this.minY}get bottom(){return this.maxY}get isPositive(){return this.maxX-this.minX>0&&this.maxY-this.minY>0}get isValid(){return this.minX+this.minY!==1/0}addVertexData(t,e,i,r){let n=this.minX,o=this.minY,a=this.maxX,h=this.maxY;r||(r=this.matrix);const l=r.a,c=r.b,u=r.c,p=r.d,d=r.tx,m=r.ty;for(let g=e;ga?y:a,h=_>h?_:h}this.minX=n,this.minY=o,this.maxX=a,this.maxY=h}containsPoint(t,e){return this.minX<=t&&this.minY<=e&&this.maxX>=t&&this.maxY>=e}toString(){return`[pixi.js:Bounds minX=${this.minX} minY=${this.minY} maxX=${this.maxX} maxY=${this.maxY} width=${this.width} height=${this.height}]`}copyFrom(t){return this.minX=t.minX,this.minY=t.minY,this.maxX=t.maxX,this.maxY=t.maxY,this}}var mo={grad:.9,turn:360,rad:360/(2*Math.PI)},mt=function(s){return typeof s=="string"?s.length>0:typeof s=="number"},X=function(s,t,e){return t===void 0&&(t=0),e===void 0&&(e=Math.pow(10,t)),Math.round(e*s)/e+0},st=function(s,t,e){return t===void 0&&(t=0),e===void 0&&(e=1),s>e?e:s>t?s:t},Er=function(s){return(s=isFinite(s)?s%360:0)>0?s:s+360},di=function(s){return{r:st(s.r,0,255),g:st(s.g,0,255),b:st(s.b,0,255),a:st(s.a)}},Qe=function(s){return{r:X(s.r),g:X(s.g),b:X(s.b),a:X(s.a,3)}},go=/^#([0-9a-f]{3,8})$/i,Ce=function(s){var t=s.toString(16);return t.length<2?"0"+t:t},Rr=function(s){var t=s.r,e=s.g,i=s.b,r=s.a,n=Math.max(t,e,i),o=n-Math.min(t,e,i),a=o?n===t?(e-i)/o:n===e?2+(i-t)/o:4+(t-e)/o:0;return{h:60*(a<0?a+6:a),s:n?o/n*100:0,v:n/255*100,a:r}},Ir=function(s){var t=s.h,e=s.s,i=s.v,r=s.a;t=t/360*6,e/=100,i/=100;var n=Math.floor(t),o=i*(1-e),a=i*(1-(t-n)*e),h=i*(1-(1-t+n)*e),l=n%6;return{r:255*[i,a,o,o,h,i][l],g:255*[h,i,i,a,o,o][l],b:255*[o,o,h,i,i,a][l],a:r}},fi=function(s){return{h:Er(s.h),s:st(s.s,0,100),l:st(s.l,0,100),a:st(s.a)}},pi=function(s){return{h:X(s.h),s:X(s.s),l:X(s.l),a:X(s.a,3)}},mi=function(s){return Ir((e=(t=s).s,{h:t.h,s:(e*=((i=t.l)<50?i:100-i)/100)>0?2*e/(i+e)*100:0,v:i+e,a:t.a}));var t,e,i},he=function(s){return{h:(t=Rr(s)).h,s:(r=(200-(e=t.s))*(i=t.v)/100)>0&&r<200?e*i/100/(r<=100?r:200-r)*100:0,l:r/2,a:t.a};var t,e,i,r},xo=/^hsla?\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s*,\s*([+-]?\d*\.?\d+)%\s*,\s*([+-]?\d*\.?\d+)%\s*(?:,\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i,yo=/^hsla?\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s+([+-]?\d*\.?\d+)%\s+([+-]?\d*\.?\d+)%\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i,_o=/^rgba?\(\s*([+-]?\d*\.?\d+)(%)?\s*,\s*([+-]?\d*\.?\d+)(%)?\s*,\s*([+-]?\d*\.?\d+)(%)?\s*(?:,\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i,bo=/^rgba?\(\s*([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i,bs={string:[[function(s){var t=go.exec(s);return t?(s=t[1]).length<=4?{r:parseInt(s[0]+s[0],16),g:parseInt(s[1]+s[1],16),b:parseInt(s[2]+s[2],16),a:s.length===4?X(parseInt(s[3]+s[3],16)/255,2):1}:s.length===6||s.length===8?{r:parseInt(s.substr(0,2),16),g:parseInt(s.substr(2,2),16),b:parseInt(s.substr(4,2),16),a:s.length===8?X(parseInt(s.substr(6,2),16)/255,2):1}:null:null},"hex"],[function(s){var t=_o.exec(s)||bo.exec(s);return t?t[2]!==t[4]||t[4]!==t[6]?null:di({r:Number(t[1])/(t[2]?100/255:1),g:Number(t[3])/(t[4]?100/255:1),b:Number(t[5])/(t[6]?100/255:1),a:t[7]===void 0?1:Number(t[7])/(t[8]?100:1)}):null},"rgb"],[function(s){var t=xo.exec(s)||yo.exec(s);if(!t)return null;var e,i,r=fi({h:(e=t[1],i=t[2],i===void 0&&(i="deg"),Number(e)*(mo[i]||1)),s:Number(t[3]),l:Number(t[4]),a:t[5]===void 0?1:Number(t[5])/(t[6]?100:1)});return mi(r)},"hsl"]],object:[[function(s){var t=s.r,e=s.g,i=s.b,r=s.a,n=r===void 0?1:r;return mt(t)&&mt(e)&&mt(i)?di({r:Number(t),g:Number(e),b:Number(i),a:Number(n)}):null},"rgb"],[function(s){var t=s.h,e=s.s,i=s.l,r=s.a,n=r===void 0?1:r;if(!mt(t)||!mt(e)||!mt(i))return null;var o=fi({h:Number(t),s:Number(e),l:Number(i),a:Number(n)});return mi(o)},"hsl"],[function(s){var t=s.h,e=s.s,i=s.v,r=s.a,n=r===void 0?1:r;if(!mt(t)||!mt(e)||!mt(i))return null;var o=function(a){return{h:Er(a.h),s:st(a.s,0,100),v:st(a.v,0,100),a:st(a.a)}}({h:Number(t),s:Number(e),v:Number(i),a:Number(n)});return Ir(o)},"hsv"]]},gi=function(s,t){for(var e=0;e=.5},s.prototype.toHex=function(){return t=Qe(this.rgba),e=t.r,i=t.g,r=t.b,o=(n=t.a)<1?Ce(X(255*n)):"","#"+Ce(e)+Ce(i)+Ce(r)+o;var t,e,i,r,n,o},s.prototype.toRgb=function(){return Qe(this.rgba)},s.prototype.toRgbString=function(){return t=Qe(this.rgba),e=t.r,i=t.g,r=t.b,(n=t.a)<1?"rgba("+e+", "+i+", "+r+", "+n+")":"rgb("+e+", "+i+", "+r+")";var t,e,i,r,n},s.prototype.toHsl=function(){return pi(he(this.rgba))},s.prototype.toHslString=function(){return t=pi(he(this.rgba)),e=t.h,i=t.s,r=t.l,(n=t.a)<1?"hsla("+e+", "+i+"%, "+r+"%, "+n+")":"hsl("+e+", "+i+"%, "+r+"%)";var t,e,i,r,n},s.prototype.toHsv=function(){return t=Rr(this.rgba),{h:X(t.h),s:X(t.s),v:X(t.v),a:X(t.a,3)};var t},s.prototype.invert=function(){return ct({r:255-(t=this.rgba).r,g:255-t.g,b:255-t.b,a:t.a});var t},s.prototype.saturate=function(t){return t===void 0&&(t=.1),ct(Je(this.rgba,t))},s.prototype.desaturate=function(t){return t===void 0&&(t=.1),ct(Je(this.rgba,-t))},s.prototype.grayscale=function(){return ct(Je(this.rgba,-1))},s.prototype.lighten=function(t){return t===void 0&&(t=.1),ct(xi(this.rgba,t))},s.prototype.darken=function(t){return t===void 0&&(t=.1),ct(xi(this.rgba,-t))},s.prototype.rotate=function(t){return t===void 0&&(t=15),this.hue(this.hue()+t)},s.prototype.alpha=function(t){return typeof t=="number"?ct({r:(e=this.rgba).r,g:e.g,b:e.b,a:t}):X(this.rgba.a,3);var e},s.prototype.hue=function(t){var e=he(this.rgba);return typeof t=="number"?ct({h:t,s:e.s,l:e.l,a:e.a}):X(e.h)},s.prototype.isEqual=function(t){return this.toHex()===ct(t).toHex()},s}(),ct=function(s){return s instanceof ws?s:new ws(s)},yi=[],So=function(s){s.forEach(function(t){yi.indexOf(t)<0&&(t(ws,bs),yi.push(t))})};function vo(s,t){var e={white:"#ffffff",bisque:"#ffe4c4",blue:"#0000ff",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",antiquewhite:"#faebd7",aqua:"#00ffff",azure:"#f0ffff",whitesmoke:"#f5f5f5",papayawhip:"#ffefd5",plum:"#dda0dd",blanchedalmond:"#ffebcd",black:"#000000",gold:"#ffd700",goldenrod:"#daa520",gainsboro:"#dcdcdc",cornsilk:"#fff8dc",cornflowerblue:"#6495ed",burlywood:"#deb887",aquamarine:"#7fffd4",beige:"#f5f5dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkkhaki:"#bdb76b",darkgray:"#a9a9a9",darkgreen:"#006400",darkgrey:"#a9a9a9",peachpuff:"#ffdab9",darkmagenta:"#8b008b",darkred:"#8b0000",darkorchid:"#9932cc",darkorange:"#ff8c00",darkslateblue:"#483d8b",gray:"#808080",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",deeppink:"#ff1493",deepskyblue:"#00bfff",wheat:"#f5deb3",firebrick:"#b22222",floralwhite:"#fffaf0",ghostwhite:"#f8f8ff",darkviolet:"#9400d3",magenta:"#ff00ff",green:"#008000",dodgerblue:"#1e90ff",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",blueviolet:"#8a2be2",forestgreen:"#228b22",lawngreen:"#7cfc00",indianred:"#cd5c5c",indigo:"#4b0082",fuchsia:"#ff00ff",brown:"#a52a2a",maroon:"#800000",mediumblue:"#0000cd",lightcoral:"#f08080",darkturquoise:"#00ced1",lightcyan:"#e0ffff",ivory:"#fffff0",lightyellow:"#ffffe0",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",linen:"#faf0e6",mediumaquamarine:"#66cdaa",lemonchiffon:"#fffacd",lime:"#00ff00",khaki:"#f0e68c",mediumseagreen:"#3cb371",limegreen:"#32cd32",mediumspringgreen:"#00fa9a",lightskyblue:"#87cefa",lightblue:"#add8e6",midnightblue:"#191970",lightpink:"#ffb6c1",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",mintcream:"#f5fffa",lightslategray:"#778899",lightslategrey:"#778899",navajowhite:"#ffdead",navy:"#000080",mediumvioletred:"#c71585",powderblue:"#b0e0e6",palegoldenrod:"#eee8aa",oldlace:"#fdf5e6",paleturquoise:"#afeeee",mediumturquoise:"#48d1cc",mediumorchid:"#ba55d3",rebeccapurple:"#663399",lightsteelblue:"#b0c4de",mediumslateblue:"#7b68ee",thistle:"#d8bfd8",tan:"#d2b48c",orchid:"#da70d6",mediumpurple:"#9370db",purple:"#800080",pink:"#ffc0cb",skyblue:"#87ceeb",springgreen:"#00ff7f",palegreen:"#98fb98",red:"#ff0000",yellow:"#ffff00",slateblue:"#6a5acd",lavenderblush:"#fff0f5",peru:"#cd853f",palevioletred:"#db7093",violet:"#ee82ee",teal:"#008080",slategray:"#708090",slategrey:"#708090",aliceblue:"#f0f8ff",darkseagreen:"#8fbc8f",darkolivegreen:"#556b2f",greenyellow:"#adff2f",seagreen:"#2e8b57",seashell:"#fff5ee",tomato:"#ff6347",silver:"#c0c0c0",sienna:"#a0522d",lavender:"#e6e6fa",lightgreen:"#90ee90",orange:"#ffa500",orangered:"#ff4500",steelblue:"#4682b4",royalblue:"#4169e1",turquoise:"#40e0d0",yellowgreen:"#9acd32",salmon:"#fa8072",saddlebrown:"#8b4513",sandybrown:"#f4a460",rosybrown:"#bc8f8f",darksalmon:"#e9967a",lightgoldenrodyellow:"#fafad2",snow:"#fffafa",lightgrey:"#d3d3d3",lightgray:"#d3d3d3",dimgray:"#696969",dimgrey:"#696969",olivedrab:"#6b8e23",olive:"#808000"},i={};for(var r in e)i[e[r]]=r;var n={};s.prototype.toName=function(o){if(!(this.rgba.a||this.rgba.r||this.rgba.g||this.rgba.b))return"transparent";var a,h,l=i[this.toHex()];if(l)return l;if(o!=null&&o.closest){var c=this.toRgb(),u=1/0,p="black";if(!n.length)for(var d in e)n[d]=new s(e[d]).toRgb();for(var m in e){var g=(a=c,h=n[m],Math.pow(a.r-h.r,2)+Math.pow(a.g-h.g,2)+Math.pow(a.b-h.b,2));gn===e[o]);if(t!==null&&e!==null){const n=Object.keys(t),o=Object.keys(e);return n.length!==o.length?!1:n.every(a=>t[a]===e[a])}return t===e}toRgba(){const[t,e,i,r]=this._components;return{r:t,g:e,b:i,a:r}}toRgb(){const[t,e,i]=this._components;return{r:t,g:e,b:i}}toRgbaString(){const[t,e,i]=this.toUint8RgbArray();return`rgba(${t},${e},${i},${this.alpha})`}toUint8RgbArray(t){const[e,i,r]=this._components;return this._arrayRgb||(this._arrayRgb=[]),t||(t=this._arrayRgb),t[0]=Math.round(e*255),t[1]=Math.round(i*255),t[2]=Math.round(r*255),t}toArray(t){this._arrayRgba||(this._arrayRgba=[]),t||(t=this._arrayRgba);const[e,i,r,n]=this._components;return t[0]=e,t[1]=i,t[2]=r,t[3]=n,t}toRgbArray(t){this._arrayRgb||(this._arrayRgb=[]),t||(t=this._arrayRgb);const[e,i,r]=this._components;return t[0]=e,t[1]=i,t[2]=r,t}toNumber(){return this._int}toBgrNumber(){const[t,e,i]=this.toUint8RgbArray();return(i<<16)+(e<<8)+t}toLittleEndianNumber(){const t=this._int;return(t>>16)+(t&65280)+((t&255)<<16)}multiply(t){const[e,i,r,n]=re._temp.setValue(t)._components;return this._components[0]*=e,this._components[1]*=i,this._components[2]*=r,this._components[3]*=n,this._refreshInt(),this._value=null,this}premultiply(t,e=!0){return e&&(this._components[0]*=t,this._components[1]*=t,this._components[2]*=t),this._components[3]=t,this._refreshInt(),this._value=null,this}toPremultiplied(t,e=!0){if(t===1)return(255<<24)+this._int;if(t===0)return e?0:this._int;let i=this._int>>16&255,r=this._int>>8&255,n=this._int&255;return e&&(i=i*t+.5|0,r=r*t+.5|0,n=n*t+.5|0),(t*255<<24)+(i<<16)+(r<<8)+n}toHex(){const t=this._int.toString(16);return`#${"000000".substring(0,6-t.length)+t}`}toHexa(){const e=Math.round(this._components[3]*255).toString(16);return this.toHex()+"00".substring(0,2-e.length)+e}setAlpha(t){return this._components[3]=this._clamp(t),this}_normalize(t){let e,i,r,n;if((typeof t=="number"||t instanceof Number)&&t>=0&&t<=16777215){const o=t;e=(o>>16&255)/255,i=(o>>8&255)/255,r=(o&255)/255,n=1}else if((Array.isArray(t)||t instanceof Float32Array)&&t.length>=3&&t.length<=4)t=this._clamp(t),[e,i,r,n=1]=t;else if((t instanceof Uint8Array||t instanceof Uint8ClampedArray)&&t.length>=3&&t.length<=4)t=this._clamp(t,0,255),[e,i,r,n=255]=t,e/=255,i/=255,r/=255,n/=255;else if(typeof t=="string"||typeof t=="object"){if(typeof t=="string"){const a=re.HEX_PATTERN.exec(t);a&&(t=`#${a[2]}`)}const o=ct(t);o.isValid()&&({r:e,g:i,b:r,a:n}=o.rgba,e/=255,i/=255,r/=255)}if(e!==void 0)this._components[0]=e,this._components[1]=i,this._components[2]=r,this._components[3]=n,this._refreshInt();else throw new Error(`Unable to convert color ${t}`)}_refreshInt(){this._clamp(this._components);const[t,e,i]=this._components;this._int=(t*255<<16)+(e*255<<8)+(i*255|0)}_clamp(t,e=0,i=1){return typeof t=="number"?Math.min(Math.max(t,e),i):(t.forEach((r,n)=>{t[n]=Math.min(Math.max(r,e),i)}),t)}static isColorLike(t){return typeof t=="number"||typeof t=="string"||t instanceof Number||t instanceof re||Array.isArray(t)||t instanceof Uint8Array||t instanceof Uint8ClampedArray||t instanceof Float32Array||t.r!==void 0&&t.g!==void 0&&t.b!==void 0||t.r!==void 0&&t.g!==void 0&&t.b!==void 0&&t.a!==void 0||t.h!==void 0&&t.s!==void 0&&t.l!==void 0||t.h!==void 0&&t.s!==void 0&&t.l!==void 0&&t.a!==void 0||t.h!==void 0&&t.s!==void 0&&t.v!==void 0||t.h!==void 0&&t.s!==void 0&&t.v!==void 0&&t.a!==void 0}};Wt.shared=new Wt;Wt._temp=new Wt;Wt.HEX_PATTERN=/^(#|0x)?(([a-f0-9]{3}){1,2}([a-f0-9]{2})?)$/i;let Y=Wt;const Po={cullArea:null,cullable:!1,cullableChildren:!0};let es=0;const _i=500;function Q(...s){es!==_i&&(es++,es===_i?console.warn("PixiJS Warning: too many warnings, no more warnings will be reported to the console by PixiJS."):console.warn("PixiJS Warning: ",...s))}const _e={_registeredResources:new Set,register(s){this._registeredResources.add(s)},unregister(s){this._registeredResources.delete(s)},release(){this._registeredResources.forEach(s=>s.clear())},get registeredCount(){return this._registeredResources.size},isRegistered(s){return this._registeredResources.has(s)},reset(){this._registeredResources.clear()}};class Mo{constructor(t,e){this._pool=[],this._count=0,this._index=0,this._classType=t,e&&this.prepopulate(e)}prepopulate(t){for(let e=0;e0?e=this._pool[--this._index]:e=new this._classType,(i=e.init)==null||i.call(e,t),e}return(t){var e;(e=t.reset)==null||e.call(t),this._pool[this._index++]=t}get totalSize(){return this._count}get totalFree(){return this._index}get totalUsed(){return this._count-this._index}clear(){if(this._pool.length>0&&this._pool[0].destroy)for(let t=0;t{const i=t[e._classType.name]?e._classType.name+e._classType.ID:e._classType.name;t[i]={free:e.totalFree,used:e.totalUsed,size:e.totalSize}}),t}clear(){this._poolsByClass.forEach(t=>t.clear()),this._poolsByClass.clear()}}const it=new Co;_e.register(it);const To={get isCachedAsTexture(){var s;return!!((s=this.renderGroup)!=null&&s.isCachedAsTexture)},cacheAsTexture(s){typeof s=="boolean"&&s===!1?this.disableRenderGroup():(this.enableRenderGroup(),this.renderGroup.enableCacheAsTexture(s===!0?{}:s))},updateCacheTexture(){var s;(s=this.renderGroup)==null||s.updateCacheTexture()},get cacheAsBitmap(){return this.isCachedAsTexture},set cacheAsBitmap(s){F("v8.6.0","cacheAsBitmap is deprecated, use cacheAsTexture instead."),this.cacheAsTexture(s)}};function Ao(s,t,e){const i=s.length;let r;if(t>=i||e===0)return;e=t+e>i?i-t:e;const n=i-e;for(r=t;r0&&i<=e){for(let a=e-1;a>=s;a--){const h=this.children[a];h&&(r.push(h),h.parent=null)}Ao(this.children,s,e);const o=this.renderGroup||this.parentRenderGroup;o&&o.removeChildren(r);for(let a=0;a0&&this._didViewChangeTick++,r}else if(i===0&&this.children.length===0)return r;throw new RangeError("removeChildren: numeric values are outside the acceptable range.")},removeChildAt(s){const t=this.getChildAt(s);return this.removeChild(t)},getChildAt(s){if(s<0||s>=this.children.length)throw new Error(`getChildAt: Index (${s}) does not exist.`);return this.children[s]},setChildIndex(s,t){if(t<0||t>=this.children.length)throw new Error(`The index ${t} supplied is out of bounds ${this.children.length}`);this.getChildIndex(s),this.addChildAt(s,t)},getChildIndex(s){const t=this.children.indexOf(s);if(t===-1)throw new Error("The supplied Container must be a child of the caller");return t},addChildAt(s,t){this.allowChildren||F(O,"addChildAt: Only Containers will be allowed to add children in v8.0.0");const{children:e}=this;if(t<0||t>e.length)throw new Error(`${s}addChildAt: The index ${t} supplied is out of bounds ${e.length}`);if(s.parent){const r=s.parent.children.indexOf(s);if(s.parent===this&&r===t)return s;r!==-1&&s.parent.children.splice(r,1)}t===e.length?e.push(s):e.splice(t,0,s),s.parent=this,s.didChange=!0,s._updateFlags=15;const i=this.renderGroup||this.parentRenderGroup;return i&&i.addChild(s),this.sortableChildren&&(this.sortDirty=!0),this.emit("childAdded",s,this,t),s.emit("added",this),s},swapChildren(s,t){if(s===t)return;const e=this.getChildIndex(s),i=this.getChildIndex(t);this.children[e]=t,this.children[i]=s;const r=this.renderGroup||this.parentRenderGroup;r&&(r.structureDidChange=!0),this._didContainerChangeTick++},removeFromParent(){var s;(s=this.parent)==null||s.removeChild(this)},reparentChild(...s){return s.length===1?this.reparentChildAt(s[0],this.children.length):(s.forEach(t=>this.reparentChildAt(t,this.children.length)),s[0])},reparentChildAt(s,t){if(s.parent===this)return this.setChildIndex(s,t),s;const e=s.worldTransform.clone();s.removeFromParent(),this.addChildAt(s,t);const i=this.worldTransform.clone();return i.invert(),e.prepend(i),s.setFromMatrix(e),s},replaceChild(s,t){s.updateLocalTransform(),this.addChildAt(t,this.getChildIndex(s)),t.setFromMatrix(s.localTransform),t.updateLocalTransform(),this.removeChild(s)}},Eo={collectRenderables(s,t,e){this.parentRenderLayer&&this.parentRenderLayer!==e||this.globalDisplayStatus<7||!this.includeInBuild||(this.sortableChildren&&this.sortChildren(),this.isSimple?this.collectRenderablesSimple(s,t,e):this.renderGroup?t.renderPipes.renderGroup.addRenderGroup(this.renderGroup,s):this.collectRenderablesWithEffects(s,t,e))},collectRenderablesSimple(s,t,e){const i=this.children,r=i.length;for(let n=0;n=0;r--){const n=this.effects[r];i[n.pipe].pop(n,this,s)}}};class bi{constructor(){this.pipe="filter",this.priority=1}destroy(){for(let t=0;t{this.add({test:t.test,maskClass:t})}))}add(t){this._tests.push(t)}getMaskEffect(t){this._initialized||this.init();for(let e=0;ee.priority-i.priority),this._markStructureAsChanged(),this._updateIsSimple())},removeEffect(s){const t=this.effects.indexOf(s);t!==-1&&(this.effects.splice(t,1),this._markStructureAsChanged(),this._updateIsSimple())},set mask(s){const t=this._maskEffect;(t==null?void 0:t.mask)!==s&&(t&&(this.removeEffect(t),Ss.returnMaskEffect(t),this._maskEffect=null),s!=null&&(this._maskEffect=Ss.getMaskEffect(s),this.addEffect(this._maskEffect)))},get mask(){var s;return(s=this._maskEffect)==null?void 0:s.mask},setMask(s){this._maskOptions={...this._maskOptions,...s},s.mask&&(this.mask=s.mask),this._markStructureAsChanged()},set filters(s){var n;!Array.isArray(s)&&s&&(s=[s]);const t=this._filterEffect||(this._filterEffect=new bi);s=s;const e=(s==null?void 0:s.length)>0,i=((n=t.filters)==null?void 0:n.length)>0,r=e!==i;s=Array.isArray(s)?s.slice(0):s,t.filters=Object.freeze(s),r&&(e?this.addEffect(t):(this.removeEffect(t),t.filters=s??null))},get filters(){var s;return(s=this._filterEffect)==null?void 0:s.filters},set filterArea(s){this._filterEffect||(this._filterEffect=new bi),this._filterEffect.filterArea=s},get filterArea(){var s;return(s=this._filterEffect)==null?void 0:s.filterArea}},Bo={label:null,get name(){return F(O,"Container.name property has been removed, use Container.label instead"),this.label},set name(s){F(O,"Container.name property has been removed, use Container.label instead"),this.label=s},getChildByName(s,t=!1){return this.getChildByLabel(s,t)},getChildByLabel(s,t=!1){const e=this.children;for(let i=0;i>16&255,i=s>>8&255,r=s&255,n=t>>16&255,o=t>>8&255,a=t&255,h=e*n/255|0,l=i*o/255|0,c=r*a/255|0;return(h<<16)+(l<<8)+c}const wi=16777215;function Si(s,t){return s===wi?t:t===wi?s:Gr(s,t)}function ze(s){return((s&255)<<16)+(s&65280)+(s>>16&255)}const Lo={getGlobalAlpha(s){if(s)return this.renderGroup?this.renderGroup.worldAlpha:this.parentRenderGroup?this.parentRenderGroup.worldAlpha*this.alpha:this.alpha;let t=this.alpha,e=this.parent;for(;e;)t*=e.alpha,e=e.parent;return t},getGlobalTransform(s=new I,t){if(t)return s.copyFrom(this.worldTransform);this.updateLocalTransform();const e=Os(this,K.get().identity());return s.appendFrom(this.localTransform,e),K.return(e),s},getGlobalTint(s){if(s)return this.renderGroup?ze(this.renderGroup.worldColor):this.parentRenderGroup?ze(Si(this.localColor,this.parentRenderGroup.worldColor)):this.tint;let t=this.localColor,e=this.parent;for(;e;)t=Si(t,e.localColor),e=e.parent;return ze(t)}};function Lr(s,t,e){return t.clear(),e||(e=I.IDENTITY),Dr(s,t,e,s,!0),t.isValid||t.set(0,0,0,0),t}function Dr(s,t,e,i,r){var h,l;let n;if(r)n=K.get(),n=e.copyTo(n);else{if(!s.visible||!s.measurable)return;s.updateLocalTransform();const c=s.localTransform;n=K.get(),n.appendFrom(c,e)}const o=t,a=!!s.effects.length;if(a&&(t=xt.get().clear()),s.boundsArea)t.addRect(s.boundsArea,n);else{s.renderPipeId&&(t.matrix=n,t.addBounds(s.bounds));const c=s.children;for(let u=0;u-1&&this.renderGroupChildren.splice(e,1),t.renderGroupParent=null}addChild(t){if(this.structureDidChange=!0,t.parentRenderGroup=this,t.updateTick=-1,t.parent===this.root?t.relativeRenderGroupDepth=1:t.relativeRenderGroupDepth=t.parent.relativeRenderGroupDepth+1,t.didChange=!0,this.onChildUpdate(t),t.renderGroup){this.addRenderGroupChild(t.renderGroup);return}t._onRender&&this.addOnRender(t);const e=t.children;for(let i=0;i0}addOnRender(t){this._onRenderContainers.push(t)}removeOnRender(t){this._onRenderContainers.splice(this._onRenderContainers.indexOf(t),1)}runOnRender(t){for(let e=0;ethis.addChild(r)),(i=t.parent)==null||i.addChild(this)}static mixin(t){F("8.8.0","Container.mixin is deprecated, please use extensions.mixin instead."),rt.mixin(ft,t)}set _didChangeId(t){this._didViewChangeTick=t>>12&4095,this._didContainerChangeTick=t&4095}get _didChangeId(){return this._didContainerChangeTick&4095|(this._didViewChangeTick&4095)<<12}addChild(...t){if(this.allowChildren||F(O,"addChild: Only Containers will be allowed to add children in v8.0.0"),t.length>1){for(let r=0;r1){for(let r=0;r-1&&(this._didViewChangeTick++,this.children.splice(i,1),this.renderGroup?this.renderGroup.removeChild(e):this.parentRenderGroup&&this.parentRenderGroup.removeChild(e),e.parentRenderLayer&&e.parentRenderLayer.detach(e),e.parent=null,this.emit("childRemoved",e,this,i),e.emit("removed",this)),e}_onUpdate(t){t&&t===this._skew&&this._updateSkew(),this._didContainerChangeTick++,!this.didChange&&(this.didChange=!0,this.parentRenderGroup&&this.parentRenderGroup.onChildUpdate(this))}set isRenderGroup(t){!!this.renderGroup!==t&&(t?this.enableRenderGroup():this.disableRenderGroup())}get isRenderGroup(){return!!this.renderGroup}enableRenderGroup(){if(this.renderGroup)return;const t=this.parentRenderGroup;t==null||t.removeChild(this),this.renderGroup=it.get(Wo,this),this.groupTransform=I.IDENTITY,t==null||t.addChild(this),this._updateIsSimple()}disableRenderGroup(){if(!this.renderGroup)return;const t=this.parentRenderGroup;t==null||t.removeChild(this),it.return(this.renderGroup),this.renderGroup=null,this.groupTransform=this.relativeGroupTransform,t==null||t.addChild(this),this._updateIsSimple()}_updateIsSimple(){this.isSimple=!this.renderGroup&&this.effects.length===0}get worldTransform(){return this._worldTransform||(this._worldTransform=new I),this.renderGroup?this._worldTransform.copyFrom(this.renderGroup.worldTransform):this.parentRenderGroup&&this._worldTransform.appendFrom(this.relativeGroupTransform,this.parentRenderGroup.worldTransform),this._worldTransform}get x(){return this._position.x}set x(t){this._position.x=t}get y(){return this._position.y}set y(t){this._position.y=t}get position(){return this._position}set position(t){this._position.copyFrom(t)}get rotation(){return this._rotation}set rotation(t){this._rotation!==t&&(this._rotation=t,this._onUpdate(this._skew))}get angle(){return this.rotation*no}set angle(t){this.rotation=t*oo}get pivot(){return this._pivot===Te&&(this._pivot=new q(this,0,0)),this._pivot}set pivot(t){this._pivot===Te&&(this._pivot=new q(this,0,0),this._origin!==Ae&&Q("Setting both a pivot and origin on a Container is not recommended. This can lead to unexpected behavior if not handled carefully.")),typeof t=="number"?this._pivot.set(t):this._pivot.copyFrom(t)}get skew(){return this._skew===ss&&(this._skew=new q(this,0,0)),this._skew}set skew(t){this._skew===ss&&(this._skew=new q(this,0,0)),this._skew.copyFrom(t)}get scale(){return this._scale===is&&(this._scale=new q(this,1,1)),this._scale}set scale(t){this._scale===is&&(this._scale=new q(this,0,0)),typeof t=="string"&&(t=parseFloat(t)),typeof t=="number"?this._scale.set(t):this._scale.copyFrom(t)}get origin(){return this._origin===Ae&&(this._origin=new q(this,0,0)),this._origin}set origin(t){this._origin===Ae&&(this._origin=new q(this,0,0),this._pivot!==Te&&Q("Setting both a pivot and origin on a Container is not recommended. This can lead to unexpected behavior if not handled carefully.")),typeof t=="number"?this._origin.set(t):this._origin.copyFrom(t)}get width(){return Math.abs(this.scale.x*this.getLocalBounds().width)}set width(t){const e=this.getLocalBounds().width;this._setWidth(t,e)}get height(){return Math.abs(this.scale.y*this.getLocalBounds().height)}set height(t){const e=this.getLocalBounds().height;this._setHeight(t,e)}getSize(t){t||(t={});const e=this.getLocalBounds();return t.width=Math.abs(this.scale.x*e.width),t.height=Math.abs(this.scale.y*e.height),t}setSize(t,e){const i=this.getLocalBounds();typeof t=="object"?(e=t.height??t.width,t=t.width):e??(e=t),t!==void 0&&this._setWidth(t,i.width),e!==void 0&&this._setHeight(e,i.height)}_updateSkew(){const t=this._rotation,e=this._skew;this._cx=Math.cos(t+e._y),this._sx=Math.sin(t+e._y),this._cy=-Math.sin(t-e._x),this._sy=Math.cos(t-e._x)}updateTransform(t){return this.position.set(typeof t.x=="number"?t.x:this.position.x,typeof t.y=="number"?t.y:this.position.y),this.scale.set(typeof t.scaleX=="number"?t.scaleX||1:this.scale.x,typeof t.scaleY=="number"?t.scaleY||1:this.scale.y),this.rotation=typeof t.rotation=="number"?t.rotation:this.rotation,this.skew.set(typeof t.skewX=="number"?t.skewX:this.skew.x,typeof t.skewY=="number"?t.skewY:this.skew.y),this.pivot.set(typeof t.pivotX=="number"?t.pivotX:this.pivot.x,typeof t.pivotY=="number"?t.pivotY:this.pivot.y),this.origin.set(typeof t.originX=="number"?t.originX:this.origin.x,typeof t.originY=="number"?t.originY:this.origin.y),this}setFromMatrix(t){t.decompose(this)}updateLocalTransform(){const t=this._didContainerChangeTick;if(this._didLocalTransformChangeId===t)return;this._didLocalTransformChangeId=t;const e=this.localTransform,i=this._scale,r=this._pivot,n=this._origin,o=this._position,a=i._x,h=i._y,l=r._x,c=r._y,u=-n._x,p=-n._y;e.a=this._cx*a,e.b=this._sx*a,e.c=this._cy*h,e.d=this._sy*h,e.tx=o._x-(l*e.a+c*e.c)+(u*e.a+p*e.c)-u,e.ty=o._y-(l*e.b+c*e.d)+(u*e.b+p*e.d)-p}set alpha(t){t!==this.localAlpha&&(this.localAlpha=t,this._updateFlags|=vi,this._onUpdate())}get alpha(){return this.localAlpha}set tint(t){const i=Y.shared.setValue(t??16777215).toBgrNumber();i!==this.localColor&&(this.localColor=i,this._updateFlags|=vi,this._onUpdate())}get tint(){return ze(this.localColor)}set blendMode(t){this.localBlendMode!==t&&(this.parentRenderGroup&&(this.parentRenderGroup.structureDidChange=!0),this._updateFlags|=Xo,this.localBlendMode=t,this._onUpdate())}get blendMode(){return this.localBlendMode}get visible(){return!!(this.localDisplayStatus&2)}set visible(t){const e=t?2:0;(this.localDisplayStatus&2)!==e&&(this.parentRenderGroup&&(this.parentRenderGroup.structureDidChange=!0),this._updateFlags|=rs,this.localDisplayStatus^=2,this._onUpdate())}get culled(){return!(this.localDisplayStatus&4)}set culled(t){const e=t?0:4;(this.localDisplayStatus&4)!==e&&(this.parentRenderGroup&&(this.parentRenderGroup.structureDidChange=!0),this._updateFlags|=rs,this.localDisplayStatus^=4,this._onUpdate())}get renderable(){return!!(this.localDisplayStatus&1)}set renderable(t){const e=t?1:0;(this.localDisplayStatus&1)!==e&&(this._updateFlags|=rs,this.localDisplayStatus^=1,this.parentRenderGroup&&(this.parentRenderGroup.structureDidChange=!0),this._onUpdate())}get isRenderable(){return this.localDisplayStatus===7&&this.groupAlpha>0}destroy(t=!1){var r;if(this.destroyed)return;this.destroyed=!0;let e;if(this.children.length&&(e=this.removeChildren(0,this.children.length)),this.removeFromParent(),this.parent=null,this._maskEffect=null,this._filterEffect=null,this.effects=null,this._position=null,this._scale=null,this._pivot=null,this._origin=null,this._skew=null,this.emit("destroyed",this),this.removeAllListeners(),(typeof t=="boolean"?t:t==null?void 0:t.children)&&e)for(let n=0;n=e.minX&&i<=e.maxX&&r>=e.minY&&r<=e.maxY}onViewUpdate(){if(this._didViewChangeTick++,this._boundsDirty=!0,this.didViewUpdate)return;this.didViewUpdate=!0;const t=this.renderGroup||this.parentRenderGroup;t&&t.onChildViewUpdate(this)}destroy(t){var e,i;super.destroy(t),this._bounds=null;for(const r in this._gpuData)(i=(e=this._gpuData[r]).destroy)==null||i.call(e);this._gpuData=null}collectRenderablesSimple(t,e,i){const{renderPipes:r}=e;r.blendMode.pushBlendMode(this,this.groupBlendMode,t),r[this.renderPipeId].addRenderable(this,t),this.didViewUpdate=!1;const o=this.children,a=o.length;for(let h=0;h{this.onViewUpdate()}}),i?this.anchor=i:e.defaultAnchor&&(this.anchor=e.defaultAnchor),this.texture=e,this.allowChildren=!1,this.roundPixels=r??!1,n!==void 0&&(this.width=n),o!==void 0&&(this.height=o)}static from(t,e=!1){return t instanceof D?new de(t):new de(D.from(t,e))}set texture(t){t||(t=D.EMPTY);const e=this._texture;e!==t&&(e&&e.dynamic&&e.off("update",this.onViewUpdate,this),t.dynamic&&t.on("update",this.onViewUpdate,this),this._texture=t,this._width&&this._setWidth(this._width,this._texture.orig.width),this._height&&this._setHeight(this._height,this._texture.orig.height),this.onViewUpdate())}get texture(){return this._texture}get visualBounds(){return po(this._visualBounds,this._anchor,this._texture),this._visualBounds}get sourceBounds(){return F("8.6.1","Sprite.sourceBounds is deprecated, use visualBounds instead."),this.visualBounds}updateBounds(){const t=this._anchor,e=this._texture,i=this._bounds,{width:r,height:n}=e.orig;i.minX=-t._x*r,i.maxX=i.minX+r,i.minY=-t._y*n,i.maxY=i.minY+n}destroy(t=!1){if(super.destroy(t),typeof t=="boolean"?t:t==null?void 0:t.texture){const i=typeof t=="boolean"?t:t==null?void 0:t.textureSource;this._texture.destroy(i)}this._texture=null,this._visualBounds=null,this._bounds=null,this._anchor=null,this._gpuData=null}get anchor(){return this._anchor}set anchor(t){typeof t=="number"?this._anchor.set(t):this._anchor.copyFrom(t)}get width(){return Math.abs(this.scale.x)*this._texture.orig.width}set width(t){this._setWidth(t,this._texture.orig.width),this._width=t}get height(){return Math.abs(this.scale.y)*this._texture.orig.height}set height(t){this._setHeight(t,this._texture.orig.height),this._height=t}getSize(t){return t||(t={}),t.width=Math.abs(this.scale.x)*this._texture.orig.width,t.height=Math.abs(this.scale.y)*this._texture.orig.height,t}setSize(t,e){typeof t=="object"?(e=t.height??t.width,t=t.width):e??(e=t),t!==void 0&&this._setWidth(t,this._texture.orig.width),e!==void 0&&this._setHeight(e,this._texture.orig.height)}}const jo=new at;function Ur(s,t,e){const i=jo;s.measurable=!0,Br(s,e,i),t.addBoundsMask(i),s.measurable=!1}function $r(s,t,e){const i=xt.get();s.measurable=!0;const r=K.get().identity(),n=Or(s,e,r);Lr(s,i,n),s.measurable=!1,t.addBoundsMask(i),K.return(r),xt.return(i)}function Or(s,t,e){return s?(s!==t&&(Or(s.parent,t,e),s.updateLocalTransform(),e.append(s.localTransform)),e):(Q("Mask bounds, renderable is not inside the root container"),e)}class Vr{constructor(t){this.priority=0,this.inverse=!1,this.pipe="alphaMask",t!=null&&t.mask&&this.init(t.mask)}init(t){this.mask=t,this.renderMaskToTexture=!(t instanceof de),this.mask.renderable=this.renderMaskToTexture,this.mask.includeInBuild=!this.renderMaskToTexture,this.mask.measurable=!1}reset(){this.mask.measurable=!0,this.mask=null}addBounds(t,e){this.inverse||Ur(this.mask,t,e)}addLocalBounds(t,e){$r(this.mask,t,e)}containsPoint(t,e){const i=this.mask;return e(i,t)}destroy(){this.reset()}static test(t){return t instanceof de}}Vr.extension=L.MaskEffect;class Wr{constructor(t){this.priority=0,this.pipe="colorMask",t!=null&&t.mask&&this.init(t.mask)}init(t){this.mask=t}destroy(){}static test(t){return typeof t=="number"}}Wr.extension=L.MaskEffect;class Yr{constructor(t){this.priority=0,this.pipe="stencilMask",t!=null&&t.mask&&this.init(t.mask)}init(t){this.mask=t,this.mask.includeInBuild=!1,this.mask.measurable=!1}reset(){this.mask.measurable=!0,this.mask.includeInBuild=!0,this.mask=null}addBounds(t,e){Ur(this.mask,t,e)}addLocalBounds(t,e){$r(this.mask,t,e)}containsPoint(t,e){const i=this.mask;return e(i,t)}destroy(){this.reset()}static test(t){return t instanceof ft}}Yr.extension=L.MaskEffect;const qo={createCanvas:(s,t)=>{const e=document.createElement("canvas");return e.width=s,e.height=t,e},createImage:()=>new Image,getCanvasRenderingContext2D:()=>CanvasRenderingContext2D,getWebGLRenderingContext:()=>WebGLRenderingContext,getNavigator:()=>navigator,getBaseUrl:()=>document.baseURI??window.location.href,getFontFaceSet:()=>document.fonts,fetch:(s,t)=>fetch(s,t),parseXML:s=>new DOMParser().parseFromString(s,"text/xml")};let Pi=qo;const et={get(){return Pi},set(s){Pi=s}};class Xr extends ht{constructor(t){t.resource||(t.resource=et.get().createCanvas()),t.width||(t.width=t.resource.width,t.autoDensity||(t.width/=t.resolution)),t.height||(t.height=t.resource.height,t.autoDensity||(t.height/=t.resolution)),super(t),this.uploadMethodId="image",this.autoDensity=t.autoDensity,this.resizeCanvas(),this.transparent=!!t.transparent}resizeCanvas(){this.autoDensity&&"style"in this.resource&&(this.resource.style.width=`${this.width}px`,this.resource.style.height=`${this.height}px`),(this.resource.width!==this.pixelWidth||this.resource.height!==this.pixelHeight)&&(this.resource.width=this.pixelWidth,this.resource.height=this.pixelHeight)}resize(t=this.width,e=this.height,i=this._resolution){const r=super.resize(t,e,i);return r&&this.resizeCanvas(),r}static test(t){return globalThis.HTMLCanvasElement&&t instanceof HTMLCanvasElement||globalThis.OffscreenCanvas&&t instanceof OffscreenCanvas}get context2D(){return this._context2D||(this._context2D=this.resource.getContext("2d"))}}Xr.extension=L.TextureSource;class $e extends ht{constructor(t){super(t),this.uploadMethodId="image",this.autoGarbageCollect=!0}static test(t){return globalThis.HTMLImageElement&&t instanceof HTMLImageElement||typeof ImageBitmap<"u"&&t instanceof ImageBitmap||globalThis.VideoFrame&&t instanceof VideoFrame}}$e.extension=L.TextureSource;var vs=(s=>(s[s.INTERACTION=50]="INTERACTION",s[s.HIGH=25]="HIGH",s[s.NORMAL=0]="NORMAL",s[s.LOW=-25]="LOW",s[s.UTILITY=-50]="UTILITY",s))(vs||{});class ns{constructor(t,e=null,i=0,r=!1){this.next=null,this.previous=null,this._destroyed=!1,this._fn=t,this._context=e,this.priority=i,this._once=r}match(t,e=null){return this._fn===t&&this._context===e}emit(t){this._fn&&(this._context?this._fn.call(this._context,t):this._fn(t));const e=this.next;return this._once&&this.destroy(!0),this._destroyed&&(this.next=null),e}connect(t){this.previous=t,t.next&&(t.next.previous=this),this.next=t.next,t.next=this}destroy(t=!1){this._destroyed=!0,this._fn=null,this._context=null,this.previous&&(this.previous.next=this.next),this.next&&(this.next.previous=this.previous);const e=this.next;return this.next=t?null:e,this.previous=null,e}}const jr=class tt{constructor(){this.autoStart=!1,this.deltaTime=1,this.lastTime=-1,this.speed=1,this.started=!1,this._requestId=null,this._maxElapsedMS=100,this._minElapsedMS=0,this._protected=!1,this._lastFrame=-1,this._head=new ns(null,null,1/0),this.deltaMS=1/tt.targetFPMS,this.elapsedMS=1/tt.targetFPMS,this._tick=t=>{this._requestId=null,this.started&&(this.update(t),this.started&&this._requestId===null&&this._head.next&&(this._requestId=requestAnimationFrame(this._tick)))}}_requestIfNeeded(){this._requestId===null&&this._head.next&&(this.lastTime=performance.now(),this._lastFrame=this.lastTime,this._requestId=requestAnimationFrame(this._tick))}_cancelIfNeeded(){this._requestId!==null&&(cancelAnimationFrame(this._requestId),this._requestId=null)}_startIfPossible(){this.started?this._requestIfNeeded():this.autoStart&&this.start()}add(t,e,i=vs.NORMAL){return this._addListener(new ns(t,e,i))}addOnce(t,e,i=vs.NORMAL){return this._addListener(new ns(t,e,i,!0))}_addListener(t){let e=this._head.next,i=this._head;if(!e)t.connect(i);else{for(;e;){if(t.priority>e.priority){t.connect(i);break}i=e,e=e.next}t.previous||t.connect(i)}return this._startIfPossible(),this}remove(t,e){let i=this._head.next;for(;i;)i.match(t,e)?i=i.destroy():i=i.next;return this._head.next||this._cancelIfNeeded(),this}get count(){if(!this._head)return 0;let t=0,e=this._head;for(;e=e.next;)t++;return t}start(){this.started||(this.started=!0,this._requestIfNeeded())}stop(){this.started&&(this.started=!1,this._cancelIfNeeded())}destroy(){if(!this._protected){this.stop();let t=this._head.next;for(;t;)t=t.destroy(!0);this._head.destroy(),this._head=null}}update(t=performance.now()){let e;if(t>this.lastTime){if(e=this.elapsedMS=t-this.lastTime,e>this._maxElapsedMS&&(e=this._maxElapsedMS),e*=this.speed,this._minElapsedMS){const n=t-this._lastFrame|0;if(n{var o;const t=et.get().createCanvas(1,1).getContext("webgl");if(!t)return"premultiply-alpha-on-upload";const e=await new Promise(a=>{const h=document.createElement("video");h.onloadeddata=()=>a(h),h.onerror=()=>a(null),h.autoplay=!1,h.crossOrigin="anonymous",h.preload="auto",h.src="data:video/webm;base64,GkXfo59ChoEBQveBAULygQRC84EIQoKEd2VibUKHgQJChYECGFOAZwEAAAAAAAHTEU2bdLpNu4tTq4QVSalmU6yBoU27i1OrhBZUrmtTrIHGTbuMU6uEElTDZ1OsggEXTbuMU6uEHFO7a1OsggG97AEAAAAAAABZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVSalmoCrXsYMPQkBNgIRMYXZmV0GETGF2ZkSJiEBEAAAAAAAAFlSua8yuAQAAAAAAAEPXgQFzxYgAAAAAAAAAAZyBACK1nIN1bmSIgQCGhVZfVlA5g4EBI+ODhAJiWgDglLCBArqBApqBAlPAgQFVsIRVuYEBElTDZ9Vzc9JjwItjxYgAAAAAAAAAAWfInEWjh0VOQ09ERVJEh49MYXZjIGxpYnZweC12cDlnyKJFo4hEVVJBVElPTkSHlDAwOjAwOjAwLjA0MDAwMDAwMAAAH0O2dcfngQCgwqGggQAAAIJJg0IAABAAFgA4JBwYSgAAICAAEb///4r+AAB1oZ2mm+6BAaWWgkmDQgAAEAAWADgkHBhKAAAgIABIQBxTu2uRu4+zgQC3iveBAfGCAXHwgQM=",h.load()});if(!e)return"premultiply-alpha-on-upload";const i=t.createTexture();t.bindTexture(t.TEXTURE_2D,i);const r=t.createFramebuffer();t.bindFramebuffer(t.FRAMEBUFFER,r),t.framebufferTexture2D(t.FRAMEBUFFER,t.COLOR_ATTACHMENT0,t.TEXTURE_2D,i,0),t.pixelStorei(t.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1),t.pixelStorei(t.UNPACK_COLORSPACE_CONVERSION_WEBGL,t.NONE),t.texImage2D(t.TEXTURE_2D,0,t.RGBA,t.RGBA,t.UNSIGNED_BYTE,e);const n=new Uint8Array(4);return t.readPixels(0,0,1,1,t.RGBA,t.UNSIGNED_BYTE,n),t.deleteFramebuffer(r),t.deleteTexture(i),(o=t.getExtension("WEBGL_lose_context"))==null||o.loseContext(),n[0]<=n[3]?"premultiplied-alpha":"premultiply-alpha-on-upload"})()),os}const We=class qr extends ht{constructor(t){super(t),this.isReady=!1,this.uploadMethodId="video",t={...qr.defaultOptions,...t},this._autoUpdate=!0,this._isConnectedToTicker=!1,this._updateFPS=t.updateFPS||0,this._msToNextUpdate=0,this.autoPlay=t.autoPlay!==!1,this.alphaMode=t.alphaMode??"premultiply-alpha-on-upload",this._videoFrameRequestCallback=this._videoFrameRequestCallback.bind(this),this._videoFrameRequestCallbackHandle=null,this._load=null,this._resolve=null,this._reject=null,this._onCanPlay=this._onCanPlay.bind(this),this._onCanPlayThrough=this._onCanPlayThrough.bind(this),this._onError=this._onError.bind(this),this._onPlayStart=this._onPlayStart.bind(this),this._onPlayStop=this._onPlayStop.bind(this),this._onSeeked=this._onSeeked.bind(this),t.autoLoad!==!1&&this.load()}updateFrame(){if(!this.destroyed){if(this._updateFPS){const t=ke.shared.elapsedMS*this.resource.playbackRate;this._msToNextUpdate=Math.floor(this._msToNextUpdate-t)}(!this._updateFPS||this._msToNextUpdate<=0)&&(this._msToNextUpdate=this._updateFPS?Math.floor(1e3/this._updateFPS):0),this.isValid&&this.update()}}_videoFrameRequestCallback(){this.updateFrame(),this.destroyed?this._videoFrameRequestCallbackHandle=null:this._videoFrameRequestCallbackHandle=this.resource.requestVideoFrameCallback(this._videoFrameRequestCallback)}get isValid(){return!!this.resource.videoWidth&&!!this.resource.videoHeight}async load(){if(this._load)return this._load;const t=this.resource,e=this.options;return(t.readyState===t.HAVE_ENOUGH_DATA||t.readyState===t.HAVE_FUTURE_DATA)&&t.width&&t.height&&(t.complete=!0),t.addEventListener("play",this._onPlayStart),t.addEventListener("pause",this._onPlayStop),t.addEventListener("seeked",this._onSeeked),this._isSourceReady()?this._mediaReady():(e.preload||t.addEventListener("canplay",this._onCanPlay),t.addEventListener("canplaythrough",this._onCanPlayThrough),t.addEventListener("error",this._onError,!0)),this.alphaMode=await Zo(),this._load=new Promise((i,r)=>{this.isValid?i(this):(this._resolve=i,this._reject=r,e.preloadTimeoutMs!==void 0&&(this._preloadTimeout=setTimeout(()=>{this._onError(new ErrorEvent(`Preload exceeded timeout of ${e.preloadTimeoutMs}ms`))})),t.load())}),this._load}_onError(t){this.resource.removeEventListener("error",this._onError,!0),this.emit("error",t),this._reject&&(this._reject(t),this._reject=null,this._resolve=null)}_isSourcePlaying(){const t=this.resource;return!t.paused&&!t.ended}_isSourceReady(){return this.resource.readyState>2}_onPlayStart(){this.isValid||this._mediaReady(),this._configureAutoUpdate()}_onPlayStop(){this._configureAutoUpdate()}_onSeeked(){this._autoUpdate&&!this._isSourcePlaying()&&(this._msToNextUpdate=0,this.updateFrame(),this._msToNextUpdate=0)}_onCanPlay(){this.resource.removeEventListener("canplay",this._onCanPlay),this._mediaReady()}_onCanPlayThrough(){this.resource.removeEventListener("canplaythrough",this._onCanPlay),this._preloadTimeout&&(clearTimeout(this._preloadTimeout),this._preloadTimeout=void 0),this._mediaReady()}_mediaReady(){const t=this.resource;this.isValid&&(this.isReady=!0,this.resize(t.videoWidth,t.videoHeight)),this._msToNextUpdate=0,this.updateFrame(),this._msToNextUpdate=0,this._resolve&&(this._resolve(this),this._resolve=null,this._reject=null),this._isSourcePlaying()?this._onPlayStart():this.autoPlay&&this.resource.play()}destroy(){this._configureAutoUpdate();const t=this.resource;t&&(t.removeEventListener("play",this._onPlayStart),t.removeEventListener("pause",this._onPlayStop),t.removeEventListener("seeked",this._onSeeked),t.removeEventListener("canplay",this._onCanPlay),t.removeEventListener("canplaythrough",this._onCanPlayThrough),t.removeEventListener("error",this._onError,!0),t.pause(),t.src="",t.load()),super.destroy()}get autoUpdate(){return this._autoUpdate}set autoUpdate(t){t!==this._autoUpdate&&(this._autoUpdate=t,this._configureAutoUpdate())}get updateFPS(){return this._updateFPS}set updateFPS(t){t!==this._updateFPS&&(this._updateFPS=t,this._configureAutoUpdate())}_configureAutoUpdate(){this._autoUpdate&&this._isSourcePlaying()?!this._updateFPS&&this.resource.requestVideoFrameCallback?(this._isConnectedToTicker&&(ke.shared.remove(this.updateFrame,this),this._isConnectedToTicker=!1,this._msToNextUpdate=0),this._videoFrameRequestCallbackHandle===null&&(this._videoFrameRequestCallbackHandle=this.resource.requestVideoFrameCallback(this._videoFrameRequestCallback))):(this._videoFrameRequestCallbackHandle!==null&&(this.resource.cancelVideoFrameCallback(this._videoFrameRequestCallbackHandle),this._videoFrameRequestCallbackHandle=null),this._isConnectedToTicker||(ke.shared.add(this.updateFrame,this),this._isConnectedToTicker=!0,this._msToNextUpdate=0)):(this._videoFrameRequestCallbackHandle!==null&&(this.resource.cancelVideoFrameCallback(this._videoFrameRequestCallbackHandle),this._videoFrameRequestCallbackHandle=null),this._isConnectedToTicker&&(ke.shared.remove(this.updateFrame,this),this._isConnectedToTicker=!1,this._msToNextUpdate=0))}static test(t){return globalThis.HTMLVideoElement&&t instanceof HTMLVideoElement}};We.extension=L.TextureSource;We.defaultOptions={...ht.defaultOptions,autoLoad:!0,autoPlay:!0,updateFPS:0,crossorigin:!0,loop:!1,muted:!0,playsinline:!0,preload:!1};We.MIME_TYPES={ogv:"video/ogg",mov:"video/quicktime",m4v:"video/mp4"};let Ko=We;const zt=(s,t,e=!1)=>(Array.isArray(s)||(s=[s]),t?s.map(i=>typeof i=="string"||e?t(i):i):s);class Qo{constructor(){this._parsers=[],this._cache=new Map,this._cacheMap=new Map}reset(){this._cacheMap.clear(),this._cache.clear()}has(t){return this._cache.has(t)}get(t){const e=this._cache.get(t);return e||Q(`[Assets] Asset id ${t} was not found in the Cache`),e}set(t,e){const i=zt(t);let r;for(let h=0;h{n.set(h,e)});const o=[...n.keys()],a={cacheKeys:o,keys:i};i.forEach(h=>{this._cacheMap.set(h,a)}),o.forEach(h=>{const l=r?r[h]:e;this._cache.has(h)&&this._cache.get(h)!==l&&Q("[Cache] already has key:",h),this._cache.set(h,n.get(h))})}remove(t){if(!this._cacheMap.has(t)){Q(`[Assets] Asset id ${t} was not found in the Cache`);return}const e=this._cacheMap.get(t);e.cacheKeys.forEach(r=>{this._cache.delete(r)}),e.keys.forEach(r=>{this._cacheMap.delete(r)})}get parsers(){return this._parsers}}const Ht=new Qo,Ps=[];rt.handleByList(L.TextureSource,Ps);function Zr(s={}){const t=s&&s.resource,e=t?s.resource:s,i=t?s:{resource:s};for(let r=0;r{Ht.has(i)&&Ht.remove(i)}),t||Ht.set(i,n),n}function ta(s,t=!1){return typeof s=="string"?Ht.get(s):s instanceof ht?new D({source:s}):Jo(s,t)}D.from=ta;ht.from=Zr;rt.add(Vr,Wr,Yr,Ko,$e,Xr,$s);var Kr=(s=>(s[s.Low=0]="Low",s[s.Normal=1]="Normal",s[s.High=2]="High",s))(Kr||{});function nt(s){if(typeof s!="string")throw new TypeError(`Path must be a string. Received ${JSON.stringify(s)}`)}function Qt(s){return s.split("?")[0].split("#")[0]}function ea(s){return s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function sa(s,t,e){return s.replace(new RegExp(ea(t),"g"),e)}function ia(s,t){let e="",i=0,r=-1,n=0,o=-1;for(let a=0;a<=s.length;++a){if(a2){const h=e.lastIndexOf("/");if(h!==e.length-1){h===-1?(e="",i=0):(e=e.slice(0,h),i=e.length-1-e.lastIndexOf("/")),r=a,n=0;continue}}else if(e.length===2||e.length===1){e="",i=0,r=a,n=0;continue}}}else e.length>0?e+=`/${s.slice(r+1,a)}`:e=s.slice(r+1,a),i=a-r-1;r=a,n=0}else o===46&&n!==-1?++n:n=-1}return e}const fe={toPosix(s){return sa(s,"\\","/")},isUrl(s){return/^https?:/.test(this.toPosix(s))},isDataUrl(s){return/^data:([a-z]+\/[a-z0-9-+.]+(;[a-z0-9-.!#$%*+.{}|~`]+=[a-z0-9-.!#$%*+.{}()_|~`]+)*)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s<>]*?)$/i.test(s)},isBlobUrl(s){return s.startsWith("blob:")},hasProtocol(s){return/^[^/:]+:/.test(this.toPosix(s))},getProtocol(s){nt(s),s=this.toPosix(s);const t=/^file:\/\/\//.exec(s);if(t)return t[0];const e=/^[^/:]+:\/{0,2}/.exec(s);return e?e[0]:""},toAbsolute(s,t,e){if(nt(s),this.isDataUrl(s)||this.isBlobUrl(s))return s;const i=Qt(this.toPosix(t??et.get().getBaseUrl())),r=Qt(this.toPosix(e??this.rootname(i)));return s=this.toPosix(s),s.startsWith("/")?fe.join(r,s.slice(1)):this.isAbsolute(s)?s:this.join(i,s)},normalize(s){if(nt(s),s.length===0)return".";if(this.isDataUrl(s)||this.isBlobUrl(s))return s;s=this.toPosix(s);let t="";const e=s.startsWith("/");this.hasProtocol(s)&&(t=this.rootname(s),s=s.slice(t.length));const i=s.endsWith("/");return s=ia(s),s.length>0&&i&&(s+="/"),e?`/${s}`:t+s},isAbsolute(s){return nt(s),s=this.toPosix(s),this.hasProtocol(s)?!0:s.startsWith("/")},join(...s){if(s.length===0)return".";let t;for(let e=0;e0)if(t===void 0)t=i;else{const r=s[e-1]??"";this.joinExtensions.includes(this.extname(r).toLowerCase())?t+=`/../${i}`:t+=`/${i}`}}return t===void 0?".":this.normalize(t)},dirname(s){if(nt(s),s.length===0)return".";s=this.toPosix(s);let t=s.charCodeAt(0);const e=t===47;let i=-1,r=!0;const n=this.getProtocol(s),o=s;s=s.slice(n.length);for(let a=s.length-1;a>=1;--a)if(t=s.charCodeAt(a),t===47){if(!r){i=a;break}}else r=!1;return i===-1?e?"/":this.isUrl(o)?n+s:n:e&&i===1?"//":n+s.slice(0,i)},rootname(s){nt(s),s=this.toPosix(s);let t="";if(s.startsWith("/")?t="/":t=this.getProtocol(s),this.isUrl(s)){const e=s.indexOf("/",t.length);e!==-1?t=s.slice(0,e):t=s,t.endsWith("/")||(t+="/")}return t},basename(s,t){nt(s),t&&nt(t),s=Qt(this.toPosix(s));let e=0,i=-1,r=!0,n;if(t!==void 0&&t.length>0&&t.length<=s.length){if(t.length===s.length&&t===s)return"";let o=t.length-1,a=-1;for(n=s.length-1;n>=0;--n){const h=s.charCodeAt(n);if(h===47){if(!r){e=n+1;break}}else a===-1&&(r=!1,a=n+1),o>=0&&(h===t.charCodeAt(o)?--o===-1&&(i=n):(o=-1,i=a))}return e===i?i=a:i===-1&&(i=s.length),s.slice(e,i)}for(n=s.length-1;n>=0;--n)if(s.charCodeAt(n)===47){if(!r){e=n+1;break}}else i===-1&&(r=!1,i=n+1);return i===-1?"":s.slice(e,i)},extname(s){nt(s),s=Qt(this.toPosix(s));let t=-1,e=0,i=-1,r=!0,n=0;for(let o=s.length-1;o>=0;--o){const a=s.charCodeAt(o);if(a===47){if(!r){e=o+1;break}continue}i===-1&&(r=!1,i=o+1),a===46?t===-1?t=o:n!==1&&(n=1):t!==-1&&(n=-1)}return t===-1||i===-1||n===0||n===1&&t===i-1&&t===e+1?"":s.slice(t,i)},parse(s){nt(s);const t={root:"",dir:"",base:"",ext:"",name:""};if(s.length===0)return t;s=Qt(this.toPosix(s));let e=s.charCodeAt(0);const i=this.isAbsolute(s);let r;t.root=this.rootname(s),i||this.hasProtocol(s)?r=1:r=0;let n=-1,o=0,a=-1,h=!0,l=s.length-1,c=0;for(;l>=r;--l){if(e=s.charCodeAt(l),e===47){if(!h){o=l+1;break}continue}a===-1&&(h=!1,a=l+1),e===46?n===-1?n=l:c!==1&&(c=1):n!==-1&&(c=-1)}return n===-1||a===-1||c===0||c===1&&n===a-1&&n===o+1?a!==-1&&(o===0&&i?t.base=t.name=s.slice(1,a):t.base=t.name=s.slice(o,a)):(o===0&&i?(t.name=s.slice(1,n),t.base=s.slice(1,a)):(t.name=s.slice(o,n),t.base=s.slice(o,a)),t.ext=s.slice(n,a)),t.dir=this.dirname(s),t},sep:"/",delimiter:":",joinExtensions:[".html"]};function Qr(s,t,e,i,r){const n=t[e];for(let o=0;o{const o=n.substring(1,n.length-1).split(",");r.push(o)}),Qr(s,r,0,e,i)}else i.push(s);return i}const Mi=s=>!Array.isArray(s);class Jr{constructor(){this._defaultBundleIdentifierOptions={connector:"-",createBundleAssetId:(t,e)=>`${t}${this._bundleIdConnector}${e}`,extractAssetIdFromBundle:(t,e)=>e.replace(`${t}${this._bundleIdConnector}`,"")},this._bundleIdConnector=this._defaultBundleIdentifierOptions.connector,this._createBundleAssetId=this._defaultBundleIdentifierOptions.createBundleAssetId,this._extractAssetIdFromBundle=this._defaultBundleIdentifierOptions.extractAssetIdFromBundle,this._assetMap={},this._preferredOrder=[],this._parsers=[],this._resolverHash={},this._bundles={}}setBundleIdentifier(t){if(this._bundleIdConnector=t.connector??this._bundleIdConnector,this._createBundleAssetId=t.createBundleAssetId??this._createBundleAssetId,this._extractAssetIdFromBundle=t.extractAssetIdFromBundle??this._extractAssetIdFromBundle,this._extractAssetIdFromBundle("foo",this._createBundleAssetId("foo","bar"))!=="bar")throw new Error("[Resolver] GenerateBundleAssetId are not working correctly")}prefer(...t){t.forEach(e=>{this._preferredOrder.push(e),e.priority||(e.priority=Object.keys(e.params))}),this._resolverHash={}}set basePath(t){this._basePath=t}get basePath(){return this._basePath}set rootPath(t){this._rootPath=t}get rootPath(){return this._rootPath}get parsers(){return this._parsers}reset(){this.setBundleIdentifier(this._defaultBundleIdentifierOptions),this._assetMap={},this._preferredOrder=[],this._resolverHash={},this._rootPath=null,this._basePath=null,this._manifest=null,this._bundles={},this._defaultSearchParams=null}setDefaultSearchParams(t){if(typeof t=="string")this._defaultSearchParams=t;else{const e=t;this._defaultSearchParams=Object.keys(e).map(i=>`${encodeURIComponent(i)}=${encodeURIComponent(e[i])}`).join("&")}}getAlias(t){const{alias:e,src:i}=t;return zt(e||i,n=>typeof n=="string"?n:Array.isArray(n)?n.map(o=>(o==null?void 0:o.src)??o):n!=null&&n.src?n.src:n,!0)}addManifest(t){this._manifest&&Q("[Resolver] Manifest already exists, this will be overwritten"),this._manifest=t,t.bundles.forEach(e=>{this.addBundle(e.name,e.assets)})}addBundle(t,e){const i=[];let r=e;Array.isArray(e)||(r=Object.entries(e).map(([n,o])=>typeof o=="string"||Array.isArray(o)?{alias:n,src:o}:{alias:n,...o})),r.forEach(n=>{const o=n.src,a=n.alias;let h;if(typeof a=="string"){const l=this._createBundleAssetId(t,a);i.push(l),h=[a,l]}else{const l=a.map(c=>this._createBundleAssetId(t,c));i.push(...l),h=[...a,...l]}this.add({...n,alias:h,src:o})}),this._bundles[t]=i}add(t){const e=[];Array.isArray(t)?e.push(...t):e.push(t);let i;i=n=>{this.hasKey(n)&&Q(`[Resolver] already has key: ${n} overwriting`)},zt(e).forEach(n=>{const{src:o}=n;let{data:a,format:h,loadParser:l,parser:c}=n;const u=zt(o).map(m=>typeof m=="string"?ra(m):Array.isArray(m)?m:[m]),p=this.getAlias(n);Array.isArray(p)?p.forEach(i):i(p);const d=[];u.forEach(m=>{m.forEach(g=>{let f={};if(typeof g!="object"){f.src=g;for(let x=0;x{this._assetMap[m]=d})})}resolveBundle(t){const e=Mi(t);t=zt(t);const i={};return t.forEach(r=>{const n=this._bundles[r];if(n){const o=this.resolve(n),a={};for(const h in o){const l=o[h];a[this._extractAssetIdFromBundle(r,h)]=l}i[r]=a}}),e?i[t[0]]:i}resolveUrl(t){const e=this.resolve(t);if(typeof t!="string"){const i={};for(const r in e)i[r]=e[r].src;return i}return e.src}resolve(t){const e=Mi(t);t=zt(t);const i={};return t.forEach(r=>{if(!this._resolverHash[r])if(this._assetMap[r]){let n=this._assetMap[r];const o=this._getPreferredOrder(n);o==null||o.priority.forEach(a=>{o.params[a].forEach(h=>{const l=n.filter(c=>c[a]?c[a]===h:!1);l.length&&(n=l)})}),this._resolverHash[r]=n[0]}else this._resolverHash[r]=this._buildResolvedAsset({alias:[r],src:r},{});i[r]=this._resolverHash[r]}),e?i[t[0]]:i}hasKey(t){return!!this._assetMap[t]}hasBundle(t){return!!this._bundles[t]}_getPreferredOrder(t){for(let e=0;en.params.format.includes(i.format));if(r)return r}return this._preferredOrder[0]}_appendDefaultSearchParams(t){if(!this._defaultSearchParams)return t;const e=/\?/.test(t)?"&":"?";return`${t}${e}${this._defaultSearchParams}`}_buildResolvedAsset(t,e){const{aliases:i,data:r,loadParser:n,parser:o,format:a}=e;return(this._basePath||this._rootPath)&&(t.src=fe.toAbsolute(t.src,this._basePath,this._rootPath)),t.alias=i??t.alias??[t.src],t.src=this._appendDefaultSearchParams(t.src),t.data={...r||{},...t.data},t.loadParser=n??t.loadParser,t.parser=o??t.parser,t.format=a??t.format??na(t.src),t}}Jr.RETINA_PREFIX=/@([0-9\.]+)x/;function na(s){return s.split(".").pop().split("?").shift().split("#").shift()}const Ci=(s,t)=>{const e=t.split("?")[1];return e&&(s+=`?${e}`),s},tn=class ne{constructor(t,e){this.linkedSheets=[];let i=t;(t==null?void 0:t.source)instanceof ht&&(i={texture:t,data:e});const{texture:r,data:n,cachePrefix:o=""}=i;this.cachePrefix=o,this._texture=r instanceof D?r:null,this.textureSource=r.source,this.textures={},this.animations={},this.data=n;const a=parseFloat(n.meta.scale);a?(this.resolution=a,r.source.resolution=this.resolution):this.resolution=r.source._resolution,this._frames=this.data.frames,this._frameKeys=Object.keys(this._frames),this._batchIndex=0,this._callback=null}parse(){return new Promise(t=>{this._callback=t,this._batchIndex=0,this._frameKeys.length<=ne.BATCH_SIZE?(this._processFrames(0),this._processAnimations(),this._parseComplete()):this._nextBatch()})}_processFrames(t){let e=t;const i=ne.BATCH_SIZE;for(;e-t{this._batchIndex*ne.BATCH_SIZE{i[r]=t}),Object.keys(t.textures).forEach(r=>{i[`${t.cachePrefix}${r}`]=t.textures[r]}),!e){const r=fe.dirname(s[0]);t.linkedSheets.forEach((n,o)=>{const a=en([`${r}/${t.data.meta.related_multi_packs[o]}`],n,!0);Object.assign(i,a)})}return i}const aa={extension:L.Asset,cache:{test:s=>s instanceof Ti,getCacheableAssets:(s,t)=>en(s,t,!1)},resolver:{extension:{type:L.ResolveParser,name:"resolveSpritesheet"},test:s=>{const e=s.split("?")[0].split("."),i=e.pop(),r=e.pop();return i==="json"&&oa.includes(r)},parse:s=>{var e;const t=s.split(".");return{resolution:parseFloat(((e=Jr.RETINA_PREFIX.exec(s))==null?void 0:e[1])??"1"),format:t[t.length-2],src:s}}},loader:{name:"spritesheetLoader",id:"spritesheet",extension:{type:L.LoadParser,priority:Kr.Normal,name:"spritesheetLoader"},async testParse(s,t){return fe.extname(t.src).toLowerCase()===".json"&&!!s.frames},async parse(s,t,e){var u,p;const{texture:i,imageFilename:r,textureOptions:n,cachePrefix:o}=(t==null?void 0:t.data)??{};let a=fe.dirname(t.src);a&&a.lastIndexOf("/")!==a.length-1&&(a+="/");let h;if(i instanceof D)h=i;else{const d=Ci(a+(r??s.meta.image),t.src);h=(await e.load([{src:d,data:n}]))[d]}const l=new Ti({texture:h.source,data:s,cachePrefix:o});await l.parse();const c=(u=s==null?void 0:s.meta)==null?void 0:u.related_multi_packs;if(Array.isArray(c)){const d=[];for(const g of c){if(typeof g!="string")continue;let f=a+g;(p=t.data)!=null&&p.ignoreMultiPack||(f=Ci(f,t.src),d.push(e.load({src:f,data:{textureOptions:n,ignoreMultiPack:!0}})))}const m=await Promise.all(d);l.linkedSheets=m,m.forEach(g=>{g.linkedSheets=[l].concat(l.linkedSheets.filter(f=>f!==g))})}return l},async unload(s,t,e){await e.unload(s.textureSource._sourceOrigin),s.destroy(!1)}}};rt.add(aa);const as=Object.create(null),Ai=Object.create(null);function Ws(s,t){let e=Ai[s];return e===void 0&&(as[t]===void 0&&(as[t]=1),Ai[s]=e=as[t]++),e}let Gt;function sn(){return(!Gt||Gt!=null&&Gt.isContextLost())&&(Gt=et.get().createCanvas().getContext("webgl",{})),Gt}let Ee;function ha(){if(!Ee){Ee="mediump";const s=sn();s&&s.getShaderPrecisionFormat&&(Ee=s.getShaderPrecisionFormat(s.FRAGMENT_SHADER,s.HIGH_FLOAT).precision?"highp":"mediump")}return Ee}function la(s,t,e){return t?s:e?(s=s.replace("out vec4 finalColor;",""),` + + #ifdef GL_ES // This checks if it is WebGL1 + #define in varying + #define finalColor gl_FragColor + #define texture texture2D + #endif + ${s} + `):` + + #ifdef GL_ES // This checks if it is WebGL1 + #define in attribute + #define out varying + #endif + ${s} + `}function ca(s,t,e){const i=e?t.maxSupportedFragmentPrecision:t.maxSupportedVertexPrecision;if(s.substring(0,9)!=="precision"){let r=e?t.requestedFragmentPrecision:t.requestedVertexPrecision;return r==="highp"&&i!=="highp"&&(r="mediump"),`precision ${r} float; +${s}`}else if(i!=="highp"&&s.substring(0,15)==="precision highp")return s.replace("precision highp","precision mediump");return s}function ua(s,t){return t?`#version 300 es +${s}`:s}const da={},fa={};function pa(s,{name:t="pixi-program"},e=!0){t=t.replace(/\s+/g,"-"),t+=e?"-fragment":"-vertex";const i=e?da:fa;return i[t]?(i[t]++,t+=`-${i[t]}`):i[t]=1,s.indexOf("#define SHADER_NAME")!==-1?s:`${`#define SHADER_NAME ${t}`} +${s}`}function ma(s,t){return t?s.replace("#version 300 es",""):s}const hs={stripVersion:ma,ensurePrecision:ca,addProgramDefines:la,setProgramName:pa,insertVersion:ua},Jt=Object.create(null),rn=class Ms{constructor(t){t={...Ms.defaultOptions,...t};const e=t.fragment.indexOf("#version 300 es")!==-1,i={stripVersion:e,ensurePrecision:{requestedFragmentPrecision:t.preferredFragmentPrecision,requestedVertexPrecision:t.preferredVertexPrecision,maxSupportedVertexPrecision:"highp",maxSupportedFragmentPrecision:ha()},setProgramName:{name:t.name},addProgramDefines:e,insertVersion:e};let r=t.fragment,n=t.vertex;Object.keys(hs).forEach(o=>{const a=i[o];r=hs[o](r,a,!0),n=hs[o](n,a,!1)}),this.fragment=r,this.vertex=n,this.transformFeedbackVaryings=t.transformFeedbackVaryings,this._key=Ws(`${this.vertex}:${this.fragment}`,"gl-program")}destroy(){this.fragment=null,this.vertex=null,this._attributeData=null,this._uniformData=null,this._uniformBlockData=null,this.transformFeedbackVaryings=null,Jt[this._cacheKey]=null}static from(t){const e=`${t.vertex}:${t.fragment}`;return Jt[e]||(Jt[e]=new Ms(t),Jt[e]._cacheKey=e),Jt[e]}};rn.defaultOptions={preferredVertexPrecision:"highp",preferredFragmentPrecision:"mediump"};let nn=rn;const ki={uint8x2:{size:2,stride:2,normalised:!1},uint8x4:{size:4,stride:4,normalised:!1},sint8x2:{size:2,stride:2,normalised:!1},sint8x4:{size:4,stride:4,normalised:!1},unorm8x2:{size:2,stride:2,normalised:!0},unorm8x4:{size:4,stride:4,normalised:!0},snorm8x2:{size:2,stride:2,normalised:!0},snorm8x4:{size:4,stride:4,normalised:!0},uint16x2:{size:2,stride:4,normalised:!1},uint16x4:{size:4,stride:8,normalised:!1},sint16x2:{size:2,stride:4,normalised:!1},sint16x4:{size:4,stride:8,normalised:!1},unorm16x2:{size:2,stride:4,normalised:!0},unorm16x4:{size:4,stride:8,normalised:!0},snorm16x2:{size:2,stride:4,normalised:!0},snorm16x4:{size:4,stride:8,normalised:!0},float16x2:{size:2,stride:4,normalised:!1},float16x4:{size:4,stride:8,normalised:!1},float32:{size:1,stride:4,normalised:!1},float32x2:{size:2,stride:8,normalised:!1},float32x3:{size:3,stride:12,normalised:!1},float32x4:{size:4,stride:16,normalised:!1},uint32:{size:1,stride:4,normalised:!1},uint32x2:{size:2,stride:8,normalised:!1},uint32x3:{size:3,stride:12,normalised:!1},uint32x4:{size:4,stride:16,normalised:!1},sint32:{size:1,stride:4,normalised:!1},sint32x2:{size:2,stride:8,normalised:!1},sint32x3:{size:3,stride:12,normalised:!1},sint32x4:{size:4,stride:16,normalised:!1}};function ga(s){return ki[s]??ki.float32}const xa={f32:"float32","vec2":"float32x2","vec3":"float32x3","vec4":"float32x4",vec2f:"float32x2",vec3f:"float32x3",vec4f:"float32x4",i32:"sint32","vec2":"sint32x2","vec3":"sint32x3","vec4":"sint32x4",u32:"uint32","vec2":"uint32x2","vec3":"uint32x3","vec4":"uint32x4",bool:"uint32","vec2":"uint32x2","vec3":"uint32x3","vec4":"uint32x4"};function ya({source:s,entryPoint:t}){const e={},i=s.indexOf(`fn ${t}`);if(i!==-1){const r=s.indexOf("->",i);if(r!==-1){const n=s.substring(i,r),o=/@location\((\d+)\)\s+([a-zA-Z0-9_]+)\s*:\s*([a-zA-Z0-9_<>]+)(?:,|\s|$)/g;let a;for(;(a=o.exec(n))!==null;){const h=xa[a[3]]??"float32";e[a[2]]={location:parseInt(a[1],10),format:h,stride:ga(h).stride,offset:0,instance:!1,start:0}}}}return e}function ls(s){var u,p;const t=/(^|[^/])@(group|binding)\(\d+\)[^;]+;/g,e=/@group\((\d+)\)/,i=/@binding\((\d+)\)/,r=/var(<[^>]+>)? (\w+)/,n=/:\s*(\w+)/,o=/struct\s+(\w+)\s*{([^}]+)}/g,a=/(\w+)\s*:\s*([\w\<\>]+)/g,h=/struct\s+(\w+)/,l=(u=s.match(t))==null?void 0:u.map(d=>({group:parseInt(d.match(e)[1],10),binding:parseInt(d.match(i)[1],10),name:d.match(r)[2],isUniform:d.match(r)[1]==="",type:d.match(n)[1]}));if(!l)return{groups:[],structs:[]};const c=((p=s.match(o))==null?void 0:p.map(d=>{const m=d.match(h)[1],g=d.match(a).reduce((f,x)=>{const[y,_]=x.split(":");return f[y.trim()]=_.trim(),f},{});return g?{name:m,members:g}:null}).filter(({name:d})=>l.some(m=>m.type===d)))??[];return{groups:l,structs:c}}var oe=(s=>(s[s.VERTEX=1]="VERTEX",s[s.FRAGMENT=2]="FRAGMENT",s[s.COMPUTE=4]="COMPUTE",s))(oe||{});function _a({groups:s}){const t=[];for(let e=0;ee.has(o.name)?!1:(e.add(o.name),!0)),n=[...s.groups,...t.groups].filter(o=>{const a=`${o.name}-${o.binding}`;return i.has(a)?!1:(i.add(a),!0)});return{structs:r,groups:n}}const te=Object.create(null);class Ye{constructor(t){var a,h;this._layoutKey=0,this._attributeLocationsKey=0;const{fragment:e,vertex:i,layout:r,gpuLayout:n,name:o}=t;if(this.name=o,this.fragment=e,this.vertex=i,e.source===i.source){const l=ls(e.source);this.structsAndGroups=l}else{const l=ls(i.source),c=ls(e.source);this.structsAndGroups=wa(l,c)}this.layout=r??ba(this.structsAndGroups),this.gpuLayout=n??_a(this.structsAndGroups),this.autoAssignGlobalUniforms=((a=this.layout[0])==null?void 0:a.globalUniforms)!==void 0,this.autoAssignLocalUniforms=((h=this.layout[1])==null?void 0:h.localUniforms)!==void 0,this._generateProgramKey()}_generateProgramKey(){const{vertex:t,fragment:e}=this,i=t.source+e.source+t.entryPoint+e.entryPoint;this._layoutKey=Ws(i,"program")}get attributeData(){return this._attributeData??(this._attributeData=ya(this.vertex)),this._attributeData}destroy(){this.gpuLayout=null,this.layout=null,this.structsAndGroups=null,this.fragment=null,this.vertex=null,te[this._cacheKey]=null}static from(t){const e=`${t.vertex.source}:${t.fragment.source}:${t.fragment.entryPoint}:${t.vertex.entryPoint}`;return te[e]||(te[e]=new Ye(t),te[e]._cacheKey=e),te[e]}}const on=["f32","i32","vec2","vec3","vec4","mat2x2","mat3x3","mat4x4","mat3x2","mat4x2","mat2x3","mat4x3","mat2x4","mat3x4","vec2","vec3","vec4"],Sa=on.reduce((s,t)=>(s[t]=!0,s),{});function va(s,t){switch(s){case"f32":return 0;case"vec2":return new Float32Array(2*t);case"vec3":return new Float32Array(3*t);case"vec4":return new Float32Array(4*t);case"mat2x2":return new Float32Array([1,0,0,1]);case"mat3x3":return new Float32Array([1,0,0,0,1,0,0,0,1]);case"mat4x4":return new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1])}return null}const an=class hn{constructor(t,e){this._touched=0,this.uid=V("uniform"),this._resourceType="uniformGroup",this._resourceId=V("resource"),this.isUniformGroup=!0,this._dirtyId=0,this.destroyed=!1,e={...hn.defaultOptions,...e},this.uniformStructures=t;const i={};for(const r in t){const n=t[r];if(n.name=r,n.size=n.size??1,!Sa[n.type]){const o=n.type.match(/^array<(\w+(?:<\w+>)?),\s*(\d+)>$/);if(o){const[,a,h]=o;throw new Error(`Uniform type ${n.type} is not supported. Use type: '${a}', size: ${h} instead.`)}throw new Error(`Uniform type ${n.type} is not supported. Supported uniform types are: ${on.join(", ")}`)}n.value??(n.value=va(n.type,n.size)),i[r]=n.value}this.uniforms=i,this._dirtyId=1,this.ubo=e.ubo,this.isStatic=e.isStatic,this._signature=Ws(Object.keys(i).map(r=>`${r}-${t[r].type}`).join("-"),"uniform-group")}update(){this._dirtyId++}};an.defaultOptions={ubo:!1,isStatic:!1};let ln=an;class He{constructor(t){this.resources=Object.create(null),this._dirty=!0;let e=0;for(const i in t){const r=t[i];this.setResource(r,e++)}this._updateKey()}_updateKey(){if(!this._dirty)return;this._dirty=!1;const t=[];let e=0;for(const i in this.resources)t[e++]=this.resources[i]._resourceId;this._key=t.join("|")}setResource(t,e){var r,n;const i=this.resources[e];t!==i&&(i&&((r=t.off)==null||r.call(t,"change",this.onResourceChange,this)),(n=t.on)==null||n.call(t,"change",this.onResourceChange,this),this.resources[e]=t,this._dirty=!0)}getResource(t){return this.resources[t]}_touch(t){const e=this.resources;for(const i in e)e[i]._touched=t}destroy(){var e;const t=this.resources;for(const i in t){const r=t[i];(e=r==null?void 0:r.off)==null||e.call(r,"change",this.onResourceChange,this)}this.resources=null}onResourceChange(t){if(this._dirty=!0,t.destroyed){const e=this.resources;for(const i in e)e[i]===t&&(e[i]=null)}else this._updateKey()}}var Cs=(s=>(s[s.WEBGL=1]="WEBGL",s[s.WEBGPU=2]="WEBGPU",s[s.BOTH=3]="BOTH",s))(Cs||{});class Ys extends pt{constructor(t){super(),this.uid=V("shader"),this._uniformBindMap=Object.create(null),this._ownedBindGroups=[];let{gpuProgram:e,glProgram:i,groups:r,resources:n,compatibleRenderers:o,groupMap:a}=t;this.gpuProgram=e,this.glProgram=i,o===void 0&&(o=0,e&&(o|=Cs.WEBGPU),i&&(o|=Cs.WEBGL)),this.compatibleRenderers=o;const h={};if(!n&&!r&&(n={}),n&&r)throw new Error("[Shader] Cannot have both resources and groups");if(!e&&r&&!a)throw new Error("[Shader] No group map or WebGPU shader provided - consider using resources instead.");if(!e&&r&&a)for(const l in a)for(const c in a[l]){const u=a[l][c];h[u]={group:l,binding:c,name:u}}else if(e&&r&&!a){const l=e.structsAndGroups.groups;a={},l.forEach(c=>{a[c.group]=a[c.group]||{},a[c.group][c.binding]=c.name,h[c.name]=c})}else if(n){r={},a={},e&&e.structsAndGroups.groups.forEach(u=>{a[u.group]=a[u.group]||{},a[u.group][u.binding]=u.name,h[u.name]=u});let l=0;for(const c in n)h[c]||(r[99]||(r[99]=new He,this._ownedBindGroups.push(r[99])),h[c]={group:99,binding:l,name:c},a[99]=a[99]||{},a[99][l]=c,l++);for(const c in n){const u=c;let p=n[c];!p.source&&!p._resourceType&&(p=new ln(p));const d=h[u];d&&(r[d.group]||(r[d.group]=new He,this._ownedBindGroups.push(r[d.group])),r[d.group].setResource(p,d.binding))}}this.groups=r,this._uniformBindMap=a,this.resources=this._buildResourceAccessor(r,h)}addResource(t,e,i){var r,n;(r=this._uniformBindMap)[e]||(r[e]={}),(n=this._uniformBindMap[e])[i]||(n[i]=t),this.groups[e]||(this.groups[e]=new He,this._ownedBindGroups.push(this.groups[e]))}_buildResourceAccessor(t,e){const i={};for(const r in e){const n=e[r];Object.defineProperty(i,n.name,{get(){return t[n.group].getResource(n.binding)},set(o){t[n.group].setResource(o,n.binding)}})}return i}destroy(t=!1){var e,i;this.emit("destroy",this),t&&((e=this.gpuProgram)==null||e.destroy(),(i=this.glProgram)==null||i.destroy()),this.gpuProgram=null,this.glProgram=null,this.removeAllListeners(),this._uniformBindMap=null,this._ownedBindGroups.forEach(r=>{r.destroy()}),this._ownedBindGroups=null,this.resources=null,this.groups=null}static from(t){const{gpu:e,gl:i,...r}=t;let n,o;return e&&(n=Ye.from(e)),i&&(o=nn.from(i)),new Ys({gpuProgram:n,glProgram:o,...r})}}const Ts=[];rt.handleByNamedList(L.Environment,Ts);async function Pa(s){if(!s)for(let t=0;t80*e){a=s[0],h=s[1];let c=a,u=h;for(let p=e;pc&&(c=d),m>u&&(u=m)}l=Math.max(c-a,u-h),l=l!==0?32767/l:0}return pe(n,o,e,a,h,l,0),o}function cn(s,t,e,i,r){let n;if(r===Ua(s,t,e,i)>0)for(let o=t;o=t;o-=i)n=Ri(o/i|0,s[o],s[o+1],n);return n&&Yt(n,n.next)&&(ge(n),n=n.next),n}function Ft(s,t){if(!s)return s;t||(t=s);let e=s,i;do if(i=!1,!e.steiner&&(Yt(e,e.next)||U(e.prev,e,e.next)===0)){if(ge(e),e=t=e.prev,e===e.next)break;i=!0}else e=e.next;while(i||e!==t);return t}function pe(s,t,e,i,r,n,o){if(!s)return;!o&&n&&Ga(s,i,r,n);let a=s;for(;s.prev!==s.next;){const h=s.prev,l=s.next;if(n?Ta(s,i,r,n):Ca(s)){t.push(h.i,s.i,l.i),ge(s),s=l.next,a=l.next;continue}if(s=l,s===a){o?o===1?(s=Aa(Ft(s),t),pe(s,t,e,i,r,n,2)):o===2&&ka(s,t,e,i,r,n):pe(Ft(s),t,e,i,r,n,1);break}}}function Ca(s){const t=s.prev,e=s,i=s.next;if(U(t,e,i)>=0)return!1;const r=t.x,n=e.x,o=i.x,a=t.y,h=e.y,l=i.y,c=Math.min(r,n,o),u=Math.min(a,h,l),p=Math.max(r,n,o),d=Math.max(a,h,l);let m=i.next;for(;m!==t;){if(m.x>=c&&m.x<=p&&m.y>=u&&m.y<=d&&ae(r,a,n,h,o,l,m.x,m.y)&&U(m.prev,m,m.next)>=0)return!1;m=m.next}return!0}function Ta(s,t,e,i){const r=s.prev,n=s,o=s.next;if(U(r,n,o)>=0)return!1;const a=r.x,h=n.x,l=o.x,c=r.y,u=n.y,p=o.y,d=Math.min(a,h,l),m=Math.min(c,u,p),g=Math.max(a,h,l),f=Math.max(c,u,p),x=As(d,m,t,e,i),y=As(g,f,t,e,i);let _=s.prevZ,b=s.nextZ;for(;_&&_.z>=x&&b&&b.z<=y;){if(_.x>=d&&_.x<=g&&_.y>=m&&_.y<=f&&_!==r&&_!==o&&ae(a,c,h,u,l,p,_.x,_.y)&&U(_.prev,_,_.next)>=0||(_=_.prevZ,b.x>=d&&b.x<=g&&b.y>=m&&b.y<=f&&b!==r&&b!==o&&ae(a,c,h,u,l,p,b.x,b.y)&&U(b.prev,b,b.next)>=0))return!1;b=b.nextZ}for(;_&&_.z>=x;){if(_.x>=d&&_.x<=g&&_.y>=m&&_.y<=f&&_!==r&&_!==o&&ae(a,c,h,u,l,p,_.x,_.y)&&U(_.prev,_,_.next)>=0)return!1;_=_.prevZ}for(;b&&b.z<=y;){if(b.x>=d&&b.x<=g&&b.y>=m&&b.y<=f&&b!==r&&b!==o&&ae(a,c,h,u,l,p,b.x,b.y)&&U(b.prev,b,b.next)>=0)return!1;b=b.nextZ}return!0}function Aa(s,t){let e=s;do{const i=e.prev,r=e.next.next;!Yt(i,r)&&dn(i,e,e.next,r)&&me(i,r)&&me(r,i)&&(t.push(i.i,e.i,r.i),ge(e),ge(e.next),e=s=r),e=e.next}while(e!==s);return Ft(e)}function ka(s,t,e,i,r,n){let o=s;do{let a=o.next.next;for(;a!==o.prev;){if(o.i!==a.i&&za(o,a)){let h=fn(o,a);o=Ft(o,o.next),h=Ft(h,h.next),pe(o,t,e,i,r,n,0),pe(h,t,e,i,r,n,0);return}a=a.next}o=o.next}while(o!==s)}function Ea(s,t,e,i){const r=[];for(let n=0,o=t.length;n=e.next.y&&e.next.y!==e.y){const u=e.x+(r-e.y)*(e.next.x-e.x)/(e.next.y-e.y);if(u<=i&&u>n&&(n=u,o=e.x=e.x&&e.x>=h&&i!==e.x&&un(ro.x||e.x===o.x&&Fa(o,e)))&&(o=e,c=u)}e=e.next}while(e!==a);return o}function Fa(s,t){return U(s.prev,s,t.prev)<0&&U(t.next,s,s.next)<0}function Ga(s,t,e,i){let r=s;do r.z===0&&(r.z=As(r.x,r.y,t,e,i)),r.prevZ=r.prev,r.nextZ=r.next,r=r.next;while(r!==s);r.prevZ.nextZ=null,r.prevZ=null,La(r)}function La(s){let t,e=1;do{let i=s,r;s=null;let n=null;for(t=0;i;){t++;let o=i,a=0;for(let l=0;l0||h>0&&o;)a!==0&&(h===0||!o||i.z<=o.z)?(r=i,i=i.nextZ,a--):(r=o,o=o.nextZ,h--),n?n.nextZ=r:s=r,r.prevZ=n,n=r;i=o}n.nextZ=null,e*=2}while(t>1);return s}function As(s,t,e,i,r){return s=(s-e)*r|0,t=(t-i)*r|0,s=(s|s<<8)&16711935,s=(s|s<<4)&252645135,s=(s|s<<2)&858993459,s=(s|s<<1)&1431655765,t=(t|t<<8)&16711935,t=(t|t<<4)&252645135,t=(t|t<<2)&858993459,t=(t|t<<1)&1431655765,s|t<<1}function Da(s){let t=s,e=s;do(t.x=(s-o)*(n-a)&&(s-o)*(i-a)>=(e-o)*(t-a)&&(e-o)*(n-a)>=(r-o)*(i-a)}function ae(s,t,e,i,r,n,o,a){return!(s===o&&t===a)&&un(s,t,e,i,r,n,o,a)}function za(s,t){return s.next.i!==t.i&&s.prev.i!==t.i&&!Ha(s,t)&&(me(s,t)&&me(t,s)&&Na(s,t)&&(U(s.prev,s,t.prev)||U(s,t.prev,t))||Yt(s,t)&&U(s.prev,s,s.next)>0&&U(t.prev,t,t.next)>0)}function U(s,t,e){return(t.y-s.y)*(e.x-t.x)-(t.x-s.x)*(e.y-t.y)}function Yt(s,t){return s.x===t.x&&s.y===t.y}function dn(s,t,e,i){const r=Ie(U(s,t,e)),n=Ie(U(s,t,i)),o=Ie(U(e,i,s)),a=Ie(U(e,i,t));return!!(r!==n&&o!==a||r===0&&Re(s,e,t)||n===0&&Re(s,i,t)||o===0&&Re(e,s,i)||a===0&&Re(e,t,i))}function Re(s,t,e){return t.x<=Math.max(s.x,e.x)&&t.x>=Math.min(s.x,e.x)&&t.y<=Math.max(s.y,e.y)&&t.y>=Math.min(s.y,e.y)}function Ie(s){return s>0?1:s<0?-1:0}function Ha(s,t){let e=s;do{if(e.i!==s.i&&e.next.i!==s.i&&e.i!==t.i&&e.next.i!==t.i&&dn(e,e.next,s,t))return!0;e=e.next}while(e!==s);return!1}function me(s,t){return U(s.prev,s,s.next)<0?U(s,t,s.next)>=0&&U(s,s.prev,t)>=0:U(s,t,s.prev)<0||U(s,s.next,t)<0}function Na(s,t){let e=s,i=!1;const r=(s.x+t.x)/2,n=(s.y+t.y)/2;do e.y>n!=e.next.y>n&&e.next.y!==e.y&&r<(e.next.x-e.x)*(n-e.y)/(e.next.y-e.y)+e.x&&(i=!i),e=e.next;while(e!==s);return i}function fn(s,t){const e=ks(s.i,s.x,s.y),i=ks(t.i,t.x,t.y),r=s.next,n=t.prev;return s.next=t,t.prev=s,e.next=r,r.prev=e,i.next=e,e.prev=i,n.next=i,i.prev=n,i}function Ri(s,t,e,i){const r=ks(s,t,e);return i?(r.next=i.next,r.prev=i,i.next.prev=r,i.next=r):(r.prev=r,r.next=r),r}function ge(s){s.next.prev=s.prev,s.prev.next=s.next,s.prevZ&&(s.prevZ.nextZ=s.nextZ),s.nextZ&&(s.nextZ.prevZ=s.prevZ)}function ks(s,t,e){return{i:s,x:t,y:e,prev:null,next:null,z:0,prevZ:null,nextZ:null,steiner:!1}}function Ua(s,t,e,i){let r=0;for(let n=t,o=e-i;n(s[s.NONE=0]="NONE",s[s.COLOR=16384]="COLOR",s[s.STENCIL=1024]="STENCIL",s[s.DEPTH=256]="DEPTH",s[s.COLOR_DEPTH=16640]="COLOR_DEPTH",s[s.COLOR_STENCIL=17408]="COLOR_STENCIL",s[s.DEPTH_STENCIL=1280]="DEPTH_STENCIL",s[s.ALL=17664]="ALL",s))(pn||{});class Oa{constructor(t){this.items=[],this._name=t}emit(t,e,i,r,n,o,a,h){const{name:l,items:c}=this;for(let u=0,p=c.length;u{this.runners[e]=new Oa(e)})}_addSystems(t){let e;for(e in t){const i=t[e];this._addSystem(i.value,i.name)}}_addSystem(t,e){const i=new t(this);if(this[e])throw new Error(`Whoops! The name "${e}" is already in use`);this[e]=i,this._systemsHash[e]=i;for(const r in this.runners)this.runners[r].add(i);return this}_addPipes(t,e){const i=e.reduce((r,n)=>(r[n.name]=n.value,r),{});t.forEach(r=>{const n=r.value,o=r.name,a=i[o];this.renderPipes[o]=new n(this,a?new a:null),this.runners.destroy.add(this.renderPipes[o])})}destroy(t=!1){this.runners.destroy.items.reverse(),this.runners.destroy.emit(t),Object.values(this.runners).forEach(e=>{e.destroy()}),(t===!0||typeof t=="object"&&t.releaseGlobalResources)&&_e.release(),this._systemsHash=null,this.renderPipes=null}generateTexture(t){return this.textureGenerator.generateTexture(t)}get roundPixels(){return!!this._roundPixels}_unsafeEvalCheck(){if(!Ma())throw new Error("Current environment does not allow unsafe-eval, please use pixi.js/unsafe-eval module to enable support.")}resetState(){this.runners.resetState.emit()}};mn.defaultOptions={resolution:1,failIfMajorPerformanceCaveat:!1,roundPixels:!1};let xn=mn,Be;function Wa(s){return Be!==void 0||(Be=(()=>{var e;const t={stencil:!0,failIfMajorPerformanceCaveat:s??xn.defaultOptions.failIfMajorPerformanceCaveat};try{if(!et.get().getWebGLRenderingContext())return!1;let r=et.get().createCanvas().getContext("webgl",t);const n=!!((e=r==null?void 0:r.getContextAttributes())!=null&&e.stencil);if(r){const o=r.getExtension("WEBGL_lose_context");o&&o.loseContext()}return r=null,n}catch{return!1}})()),Be}let Fe;async function Ya(s={}){return Fe!==void 0||(Fe=await(async()=>{const t=et.get().getNavigator().gpu;if(!t)return!1;try{return await(await t.requestAdapter(s)).requestDevice(),!0}catch{return!1}})()),Fe}const Ii=["webgl","webgpu","canvas"];async function Xa(s){let t=[];s.preference?(t.push(s.preference),Ii.forEach(n=>{n!==s.preference&&t.push(n)})):t=Ii.slice();let e,i={};for(let n=0;n{const{WebGPURenderer:h}=await import("./WebGPURenderer-CtyVoQqf.js");return{WebGPURenderer:h}},__vite__mapDeps([3,2,4]));e=a,i={...s,...s.webgpu};break}else if(o==="webgl"&&Wa(s.failIfMajorPerformanceCaveat??xn.defaultOptions.failIfMajorPerformanceCaveat)){const{WebGLRenderer:a}=await Ne(async()=>{const{WebGLRenderer:h}=await import("./WebGLRenderer-BVO0qznQ.js");return{WebGLRenderer:h}},__vite__mapDeps([5,2,4]));e=a,i={...s,...s.webgl};break}else if(o==="canvas")throw i={...s},new Error("CanvasRenderer is not yet implemented")}if(delete i.webgpu,delete i.webgl,!e)throw new Error("No available renderer for the current environment");const r=new e;return await r.init(i),r}const yn="8.13.2";class _n{static init(){var t;(t=globalThis.__PIXI_APP_INIT__)==null||t.call(globalThis,this,yn)}static destroy(){}}_n.extension=L.Application;class ja{constructor(t){this._renderer=t}init(){var t;(t=globalThis.__PIXI_RENDERER_INIT__)==null||t.call(globalThis,this._renderer,yn)}destroy(){this._renderer=null}}ja.extension={type:[L.WebGLSystem,L.WebGPUSystem],name:"initHook",priority:-10};const bn=class Es{constructor(...t){this.stage=new ft,t[0]!==void 0&&F(O,"Application constructor options are deprecated, please use Application.init() instead.")}async init(t){t={...t},this.renderer=await Xa(t),Es._plugins.forEach(e=>{e.init.call(this,t)})}render(){this.renderer.render({container:this.stage})}get canvas(){return this.renderer.canvas}get view(){return F(O,"Application.view is deprecated, please use Application.canvas instead."),this.renderer.canvas}get screen(){return this.renderer.screen}destroy(t=!1,e=!1){const i=Es._plugins.slice(0);i.reverse(),i.forEach(r=>{r.destroy.call(this)}),this.stage.destroy(e),this.stage=null,this.renderer.destroy(t),this.renderer=null}};bn._plugins=[];let wn=bn;rt.handleByList(L.Application,wn._plugins);rt.add(_n);/** + * tiny-lru + * + * @copyright 2025 Jason Mulligan + * @license BSD-3-Clause + * @version 11.4.5 + */class qa{constructor(t=0,e=0,i=!1){this.first=null,this.items=Object.create(null),this.last=null,this.max=t,this.resetTtl=i,this.size=0,this.ttl=e}clear(){return this.first=null,this.items=Object.create(null),this.last=null,this.size=0,this}delete(t){if(this.has(t)){const e=this.items[t];delete this.items[t],this.size--,e.prev!==null&&(e.prev.next=e.next),e.next!==null&&(e.next.prev=e.prev),this.first===e&&(this.first=e.next),this.last===e&&(this.last=e.prev)}return this}entries(t=this.keys()){return t.map(e=>[e,this.get(e)])}evict(t=!1){if(t||this.size>0){const e=this.first;delete this.items[e.key],--this.size===0?(this.first=null,this.last=null):(this.first=e.next,this.first.prev=null)}return this}expiresAt(t){let e;return this.has(t)&&(e=this.items[t].expiry),e}get(t){const e=this.items[t];if(e!==void 0){if(this.ttl>0&&e.expiry<=Date.now()){this.delete(t);return}return this.moveToEnd(e),e.value}}has(t){return t in this.items}moveToEnd(t){this.last!==t&&(t.prev!==null&&(t.prev.next=t.next),t.next!==null&&(t.next.prev=t.prev),this.first===t&&(this.first=t.next),t.prev=this.last,t.next=null,this.last!==null&&(this.last.next=t),this.last=t,this.first===null&&(this.first=t))}keys(){const t=[];let e=this.first;for(;e!==null;)t.push(e.key),e=e.next;return t}setWithEvicted(t,e,i=this.resetTtl){let r=null;if(this.has(t))this.set(t,e,!0,i);else{this.max>0&&this.size===this.max&&(r={...this.first},this.evict(!0));let n=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:e};++this.size===1?this.first=n:this.last.next=n,this.last=n}return r}set(t,e,i=!1,r=this.resetTtl){let n=this.items[t];return i||n!==void 0?(n.value=e,i===!1&&r&&(n.expiry=this.ttl>0?Date.now()+this.ttl:this.ttl),this.moveToEnd(n)):(this.max>0&&this.size===this.max&&this.evict(!0),n=this.items[t]={expiry:this.ttl>0?Date.now()+this.ttl:this.ttl,key:t,prev:this.last,next:null,value:e},++this.size===1?this.first=n:this.last.next=n,this.last=n),this}values(t=this.keys()){return t.map(e=>this.get(e))}}function Za(s=1e3,t=0,e=!1){if(isNaN(s)||s<0)throw new TypeError("Invalid max value");if(isNaN(t)||t<0)throw new TypeError("Invalid ttl value");if(typeof e!="boolean")throw new TypeError("Invalid resetTtl value");return new qa(s,t,e)}const Ka=["serif","sans-serif","monospace","cursive","fantasy","system-ui"];function Sn(s){const t=typeof s.fontSize=="number"?`${s.fontSize}px`:s.fontSize;let e=s.fontFamily;Array.isArray(s.fontFamily)||(e=s.fontFamily.split(","));for(let i=e.length-1;i>=0;i--){let r=e[i].trim();!/([\"\'])[^\'\"]+\1/.test(r)&&!Ka.includes(r)&&(r=`"${r}"`),e[i]=r}return`${s.fontStyle} ${s.fontVariant} ${s.fontWeight} ${t} ${e.join(",")}`}const cs={willReadFrequently:!0},lt=class T{static get experimentalLetterSpacingSupported(){let t=T._experimentalLetterSpacingSupported;if(t===void 0){const e=et.get().getCanvasRenderingContext2D().prototype;t=T._experimentalLetterSpacingSupported="letterSpacing"in e||"textLetterSpacing"in e}return t}constructor(t,e,i,r,n,o,a,h,l){this.text=t,this.style=e,this.width=i,this.height=r,this.lines=n,this.lineWidths=o,this.lineHeight=a,this.maxLineWidth=h,this.fontProperties=l}static measureText(t=" ",e,i=T._canvas,r=e.wordWrap){var y;const n=`${t}-${e.styleKey}-wordWrap-${r}`;if(T._measurementCache.has(n))return T._measurementCache.get(n);const o=Sn(e),a=T.measureFont(o);a.fontSize===0&&(a.fontSize=e.fontSize,a.ascent=e.fontSize);const h=T.__context;h.font=o;const c=(r?T._wordWrap(t,e,i):t).split(/(?:\r\n|\r|\n)/),u=new Array(c.length);let p=0;for(let _=0;_0)if(r)o-=e,l-=e;else{const c=(T.graphemeSegmenter(t).length-1)*e;o+=c,l+=c}return Math.max(o,l)}static _wordWrap(t,e,i=T._canvas){const r=i.getContext("2d",cs);let n=0,o="",a="";const h=Object.create(null),{letterSpacing:l,whiteSpace:c}=e,u=T._collapseSpaces(c),p=T._collapseNewlines(c);let d=!u;const m=e.wordWrapWidth+l,g=T._tokenize(t);for(let f=0;fm)if(o!==""&&(a+=T._addLine(o),o="",n=0),T.canBreakWords(x,e.breakWords)){const _=T.wordWrapSplit(x);for(let b=0;b<_.length;b++){let v=_[b],S=v,w=1;for(;_[b+w];){const k=_[b+w];if(!T.canBreakChars(S,k,x,b,e.breakWords))v+=k;else break;S=k,w++}b+=w-1;const R=T._getFromCache(v,l,h,r);R+n>m&&(a+=T._addLine(o),d=!1,o="",n=0),o+=v,n+=R}}else{o.length>0&&(a+=T._addLine(o),o="",n=0);const _=f===g.length-1;a+=T._addLine(x,!_),d=!1,o="",n=0}else y+n>m&&(d=!1,a+=T._addLine(o),o="",n=0),(o.length>0||!T.isBreakingSpace(x)||d)&&(o+=x,n+=y)}return a+=T._addLine(o,!1),a}static _addLine(t,e=!0){return t=T._trimRight(t),t=e?`${t} +`:t,t}static _getFromCache(t,e,i,r){let n=i[t];return typeof n!="number"&&(n=T._measureText(t,e,r)+e,i[t]=n),n}static _collapseSpaces(t){return t==="normal"||t==="pre-line"}static _collapseNewlines(t){return t==="normal"}static _trimRight(t){if(typeof t!="string")return"";for(let e=t.length-1;e>=0;e--){const i=t[e];if(!T.isBreakingSpace(i))break;t=t.slice(0,-1)}return t}static _isNewline(t){return typeof t!="string"?!1:T._newlines.includes(t.charCodeAt(0))}static isBreakingSpace(t,e){return typeof t!="string"?!1:T._breakingSpaces.includes(t.charCodeAt(0))}static _tokenize(t){const e=[];let i="";if(typeof t!="string")return e;for(let r=0;r{if(typeof(Intl==null?void 0:Intl.Segmenter)=="function"){const s=new Intl.Segmenter;return t=>{const e=s.segment(t),i=[];let r=0;for(const n of e)i[r++]=n.segment;return i}}return s=>[...s]})();lt.experimentalLetterSpacing=!1;lt._fonts={};lt._newlines=[10,13];lt._breakingSpaces=[9,32,8192,8193,8194,8195,8196,8197,8198,8200,8201,8202,8287,12288];lt._measurementCache=Za(1e3);let Nt=lt;const Bi=[{offset:0,color:"white"},{offset:1,color:"black"}],Xs=class Rs{constructor(...t){this.uid=V("fillGradient"),this._tick=0,this.type="linear",this.colorStops=[];let e=Qa(t);e={...e.type==="radial"?Rs.defaultRadialOptions:Rs.defaultLinearOptions,...Mr(e)},this._textureSize=e.textureSize,this._wrapMode=e.wrapMode,e.type==="radial"?(this.center=e.center,this.outerCenter=e.outerCenter??this.center,this.innerRadius=e.innerRadius,this.outerRadius=e.outerRadius,this.scale=e.scale,this.rotation=e.rotation):(this.start=e.start,this.end=e.end),this.textureSpace=e.textureSpace,this.type=e.type,e.colorStops.forEach(r=>{this.addColorStop(r.offset,r.color)})}addColorStop(t,e){return this.colorStops.push({offset:t,color:Y.shared.setValue(e).toHexa()}),this}buildLinearGradient(){if(this.texture)return;let{x:t,y:e}=this.start,{x:i,y:r}=this.end,n=i-t,o=r-e;const a=n<0||o<0;if(this._wrapMode==="clamp-to-edge"){if(n<0){const f=t;t=i,i=f,n*=-1}if(o<0){const f=e;e=r,r=f,o*=-1}}const h=this.colorStops.length?this.colorStops:Bi,l=this._textureSize,{canvas:c,context:u}=Gi(l,1),p=a?u.createLinearGradient(this._textureSize,0,0,0):u.createLinearGradient(0,0,this._textureSize,0);Fi(p,h),u.fillStyle=p,u.fillRect(0,0,l,1),this.texture=new D({source:new $e({resource:c,addressMode:this._wrapMode})});const d=Math.sqrt(n*n+o*o),m=Math.atan2(o,n),g=new I;g.scale(d/l,1),g.rotate(m),g.translate(t,e),this.textureSpace==="local"&&g.scale(l,l),this.transform=g}buildGradient(){this.texture||this._tick++,this.type==="linear"?this.buildLinearGradient():this.buildRadialGradient()}buildRadialGradient(){if(this.texture)return;const t=this.colorStops.length?this.colorStops:Bi,e=this._textureSize,{canvas:i,context:r}=Gi(e,e),{x:n,y:o}=this.center,{x:a,y:h}=this.outerCenter,l=this.innerRadius,c=this.outerRadius,u=a-c,p=h-c,d=e/(c*2),m=(n-u)*d,g=(o-p)*d,f=r.createRadialGradient(m,g,l*d,(a-u)*d,(h-p)*d,c*d);Fi(f,t),r.fillStyle=t[t.length-1].color,r.fillRect(0,0,e,e),r.fillStyle=f,r.translate(m,g),r.rotate(this.rotation),r.scale(1,this.scale),r.translate(-m,-g),r.fillRect(0,0,e,e),this.texture=new D({source:new $e({resource:i,addressMode:this._wrapMode})});const x=new I;x.scale(1/d,1/d),x.translate(u,p),this.textureSpace==="local"&&x.scale(e,e),this.transform=x}destroy(){var t;(t=this.texture)==null||t.destroy(!0),this.texture=null,this.transform=null,this.colorStops=[],this.start=null,this.end=null,this.center=null,this.outerCenter=null}get styleKey(){return`fill-gradient-${this.uid}-${this._tick}`}};Xs.defaultLinearOptions={start:{x:0,y:0},end:{x:0,y:1},colorStops:[],textureSpace:"local",type:"linear",textureSize:256,wrapMode:"clamp-to-edge"};Xs.defaultRadialOptions={center:{x:.5,y:.5},innerRadius:0,outerRadius:.5,colorStops:[],scale:1,textureSpace:"local",type:"radial",textureSize:256,wrapMode:"clamp-to-edge"};let _t=Xs;function Fi(s,t){for(let e=0;e2&&(t.push([i].concat(r.splice(0,2))),n="l",i=i=="m"?"l":"L");;){if(r.length==us[n])return r.unshift(i),t.push(r);if(r.length0&&(r=i.pop(),r?(n=r.startX,o=r.startY):(n=0,o=0)),r=null;break;default:Q(`Unknown SVG path command: ${l}`)}l!=="Z"&&l!=="z"&&r===null&&(r={startX:n,startY:o},i.push(r))}return t}class js{constructor(t=0,e=0,i=0){this.type="circle",this.x=t,this.y=e,this.radius=i}clone(){return new js(this.x,this.y,this.radius)}contains(t,e){if(this.radius<=0)return!1;const i=this.radius*this.radius;let r=this.x-t,n=this.y-e;return r*=r,n*=n,r+n<=i}strokeContains(t,e,i,r=.5){if(this.radius===0)return!1;const n=this.x-t,o=this.y-e,a=this.radius,h=(1-r)*i,l=Math.sqrt(n*n+o*o);return l<=a+h&&l>a-(i-h)}getBounds(t){return t||(t=new W),t.x=this.x-this.radius,t.y=this.y-this.radius,t.width=this.radius*2,t.height=this.radius*2,t}copyFrom(t){return this.x=t.x,this.y=t.y,this.radius=t.radius,this}copyTo(t){return t.copyFrom(this),t}toString(){return`[pixi.js/math:Circle x=${this.x} y=${this.y} radius=${this.radius}]`}}class qs{constructor(t=0,e=0,i=0,r=0){this.type="ellipse",this.x=t,this.y=e,this.halfWidth=i,this.halfHeight=r}clone(){return new qs(this.x,this.y,this.halfWidth,this.halfHeight)}contains(t,e){if(this.halfWidth<=0||this.halfHeight<=0)return!1;let i=(t-this.x)/this.halfWidth,r=(e-this.y)/this.halfHeight;return i*=i,r*=r,i+r<=1}strokeContains(t,e,i,r=.5){const{halfWidth:n,halfHeight:o}=this;if(n<=0||o<=0)return!1;const a=i*(1-r),h=i-a,l=n-h,c=o-h,u=n+a,p=o+a,d=t-this.x,m=e-this.y,g=d*d/(l*l)+m*m/(c*c),f=d*d/(u*u)+m*m/(p*p);return g>1&&f<=1}getBounds(t){return t||(t=new W),t.x=this.x-this.halfWidth,t.y=this.y-this.halfHeight,t.width=this.halfWidth*2,t.height=this.halfHeight*2,t}copyFrom(t){return this.x=t.x,this.y=t.y,this.halfWidth=t.halfWidth,this.halfHeight=t.halfHeight,this}copyTo(t){return t.copyFrom(this),t}toString(){return`[pixi.js/math:Ellipse x=${this.x} y=${this.y} halfWidth=${this.halfWidth} halfHeight=${this.halfHeight}]`}}function oh(s,t,e,i,r,n){const o=s-e,a=t-i,h=r-e,l=n-i,c=o*h+a*l,u=h*h+l*l;let p=-1;u!==0&&(p=c/u);let d,m;p<0?(d=e,m=i):p>1?(d=r,m=n):(d=e+p*h,m=i+p*l);const g=s-d,f=t-m;return g*g+f*f}let ah,hh;class le{constructor(...t){this.type="polygon";let e=Array.isArray(t[0])?t[0]:t;if(typeof e[0]!="number"){const i=[];for(let r=0,n=e.length;re!=c>e&&t<(l-a)*((e-h)/(c-h))+a&&(i=!i)}return i}strokeContains(t,e,i,r=.5){const n=i*i,o=n*(1-r),a=n-o,{points:h}=this,l=h.length-(this.closePath?0:2);for(let c=0;cr?l:r,n=co?c:o}return t.x=i,t.width=r-i,t.y=n,t.height=o-n,t}copyFrom(t){return this.points=t.points.slice(),this.closePath=t.closePath,this}copyTo(t){return t.copyFrom(this),t}toString(){return`[pixi.js/math:PolygoncloseStroke=${this.closePath}points=${this.points.reduce((t,e)=>`${t}, ${e}`,"")}]`}get lastX(){return this.points[this.points.length-2]}get lastY(){return this.points[this.points.length-1]}get x(){return F("8.11.0","Polygon.lastX is deprecated, please use Polygon.lastX instead."),this.points[this.points.length-2]}get y(){return F("8.11.0","Polygon.y is deprecated, please use Polygon.lastY instead."),this.points[this.points.length-1]}get startX(){return this.points[0]}get startY(){return this.points[1]}}const Ge=(s,t,e,i,r,n,o)=>{const a=s-e,h=t-i,l=Math.sqrt(a*a+h*h);return l>=r-n&&l<=r+o};class Zs{constructor(t=0,e=0,i=0,r=0,n=20){this.type="roundedRectangle",this.x=t,this.y=e,this.width=i,this.height=r,this.radius=n}getBounds(t){return t||(t=new W),t.x=this.x,t.y=this.y,t.width=this.width,t.height=this.height,t}clone(){return new Zs(this.x,this.y,this.width,this.height,this.radius)}copyFrom(t){return this.x=t.x,this.y=t.y,this.width=t.width,this.height=t.height,this}copyTo(t){return t.copyFrom(this),t}contains(t,e){if(this.width<=0||this.height<=0)return!1;if(t>=this.x&&t<=this.x+this.width&&e>=this.y&&e<=this.y+this.height){const i=Math.max(0,Math.min(this.radius,Math.min(this.width,this.height)/2));if(e>=this.y+i&&e<=this.y+this.height-i||t>=this.x+i&&t<=this.x+this.width-i)return!0;let r=t-(this.x+i),n=e-(this.y+i);const o=i*i;if(r*r+n*n<=o||(r=t-(this.x+this.width-i),r*r+n*n<=o)||(n=e-(this.y+this.height-i),r*r+n*n<=o)||(r=t-(this.x+i),r*r+n*n<=o))return!0}return!1}strokeContains(t,e,i,r=.5){const{x:n,y:o,width:a,height:h,radius:l}=this,c=i*(1-r),u=i-c,p=n+l,d=o+l,m=a-l*2,g=h-l*2,f=n+a,x=o+h;return(t>=n-c&&t<=n+u||t>=f-u&&t<=f+c)&&e>=d&&e<=d+g||(e>=o-c&&e<=o+u||e>=x-u&&e<=x+c)&&t>=p&&t<=p+m?!0:tf-l&&ef-l&&e>x-l&&Ge(t,e,f-l,x-l,l,u,c)||tx-l&&Ge(t,e,p,x-l,l,u,c)}toString(){return`[pixi.js/math:RoundedRectangle x=${this.x} y=${this.y}width=${this.width} height=${this.height} radius=${this.radius}]`}}const vn={};function lh(s,t,e){let i=2166136261;for(let r=0;r>>=0;return vn[i]||ch(s,t,i,e)}function ch(s,t,e,i){const r={};let n=0;for(let a=0;a0){const o=new Uint8Array(s,e*8,n);new Uint8Array(t,e*8,n).set(o)}}const uh={normal:"normal-npm",add:"add-npm",screen:"screen-npm"};var dh=(s=>(s[s.DISABLED=0]="DISABLED",s[s.RENDERING_MASK_ADD=1]="RENDERING_MASK_ADD",s[s.MASK_ACTIVE=2]="MASK_ACTIVE",s[s.INVERSE_MASK_ACTIVE=3]="INVERSE_MASK_ACTIVE",s[s.RENDERING_MASK_REMOVE=4]="RENDERING_MASK_REMOVE",s[s.NONE=5]="NONE",s))(dh||{});function Hi(s,t){return t.alphaMode==="no-premultiply-alpha"&&uh[s]||s}const fh=["precision mediump float;","void main(void){","float test = 0.1;","%forloop%","gl_FragColor = vec4(0.0);","}"].join(` +`);function ph(s){let t="";for(let e=0;e0&&(t+=` +else `),e{if(ce.length>0)for(const s of ce)s&&s.destroy();ce.length=0,Oe=0}});function Ni(){return Oe>0?ce[--Oe]:new yh}function Ui(s){ce[Oe++]=s}let se=0;const Pn=class Mn{constructor(t){this.uid=V("batcher"),this.dirty=!0,this.batchIndex=0,this.batches=[],this._elements=[],t={...Mn.defaultOptions,...t},t.maxTextures||(F("v8.8.0","maxTextures is a required option for Batcher now, please pass it in the options"),t.maxTextures=gh());const{maxTextures:e,attributesInitialSize:i,indicesInitialSize:r}=t;this.attributeBuffer=new Di(i*4),this.indexBuffer=new Uint16Array(r),this.maxTextures=e}begin(){this.elementSize=0,this.elementStart=0,this.indexSize=0,this.attributeSize=0;for(let t=0;tthis.attributeBuffer.size&&this._resizeAttributeBuffer(this.attributeSize*4),this.indexSize>this.indexBuffer.length&&this._resizeIndexBuffer(this.indexSize);const h=this.attributeBuffer.float32View,l=this.attributeBuffer.uint32View,c=this.indexBuffer;let u=this._batchIndexSize,p=this._batchIndexStart,d="startBatch";const m=this.maxTextures;for(let g=this.elementStart;g=m||b)&&(this._finishBatch(i,p,u-p,r,o,a,t,d),d="renderBatch",p=u,o=_,a=f.topology,i=Ni(),r=i.textures,r.clear(),++se),f._textureId=y._textureBindLocation=r.count,r.ids[y.uid]=r.count,r.textures[r.count++]=y,f._batch=i,u+=f.indexSize,f.packAsQuad?(this.packQuadAttributes(f,h,l,f._attributeStart,f._textureId),this.packQuadIndex(c,f._indexStart,f._attributeStart/this.vertexSize)):(this.packAttributes(f,h,l,f._attributeStart,f._textureId),this.packIndex(f,c,f._indexStart,f._attributeStart/this.vertexSize))}r.count>0&&(this._finishBatch(i,p,u-p,r,o,a,t,d),p=u,++se),this.elementStart=this.elementSize,this._batchIndexStart=p,this._batchIndexSize=u}_finishBatch(t,e,i,r,n,o,a,h){t.gpuBindGroup=null,t.bindGroup=null,t.action=h,t.batcher=this,t.textures=r,t.blendMode=n,t.topology=o,t.start=e,t.size=i,++se,this.batches[this.batchIndex++]=t,a.add(t)}finish(t){this.break(t)}ensureAttributeBuffer(t){t*4<=this.attributeBuffer.size||this._resizeAttributeBuffer(t*4)}ensureIndexBuffer(t){t<=this.indexBuffer.length||this._resizeIndexBuffer(t)}_resizeAttributeBuffer(t){const e=Math.max(t,this.attributeBuffer.size*2),i=new Di(e);zi(this.attributeBuffer.rawBinaryData,i.rawBinaryData),this.attributeBuffer=i}_resizeIndexBuffer(t){const e=this.indexBuffer;let i=Math.max(t,e.length*1.5);i+=i%2;const r=i>65535?new Uint32Array(i):new Uint16Array(i);if(r.BYTES_PER_ELEMENT!==e.BYTES_PER_ELEMENT)for(let n=0;n(s[s.MAP_READ=1]="MAP_READ",s[s.MAP_WRITE=2]="MAP_WRITE",s[s.COPY_SRC=4]="COPY_SRC",s[s.COPY_DST=8]="COPY_DST",s[s.INDEX=16]="INDEX",s[s.VERTEX=32]="VERTEX",s[s.UNIFORM=64]="UNIFORM",s[s.STORAGE=128]="STORAGE",s[s.INDIRECT=256]="INDIRECT",s[s.QUERY_RESOLVE=512]="QUERY_RESOLVE",s[s.STATIC=1024]="STATIC",s))(J||{});class xe extends pt{constructor(t){let{data:e,size:i}=t;const{usage:r,label:n,shrinkToFit:o}=t;super(),this.uid=V("buffer"),this._resourceType="buffer",this._resourceId=V("resource"),this._touched=0,this._updateID=1,this._dataInt32=null,this.shrinkToFit=!0,this.destroyed=!1,e instanceof Array&&(e=new Float32Array(e)),this._data=e,i??(i=e==null?void 0:e.byteLength);const a=!!e;this.descriptor={size:i,usage:r,mappedAtCreation:a,label:n},this.shrinkToFit=o??!0}get data(){return this._data}set data(t){this.setDataWithSize(t,t.length,!0)}get dataInt32(){return this._dataInt32||(this._dataInt32=new Int32Array(this.data.buffer)),this._dataInt32}get static(){return!!(this.descriptor.usage&J.STATIC)}set static(t){t?this.descriptor.usage|=J.STATIC:this.descriptor.usage&=~J.STATIC}setDataWithSize(t,e,i){if(this._updateID++,this._updateSize=e*t.BYTES_PER_ELEMENT,this._data===t){i&&this.emit("update",this);return}const r=this._data;if(this._data=t,this._dataInt32=null,!r||r.length!==t.length){!this.shrinkToFit&&r&&t.byteLengtha&&(a=d),m>h&&(h=m),de.destroy()),this.attributes=null,this.buffers=null,this.indexBuffer=null,this._bounds=null}}const vh=new Float32Array(1),Ph=new Uint32Array(1);class Mh extends Sh{constructor(){const e=new xe({data:vh,label:"attribute-batch-buffer",usage:J.VERTEX|J.COPY_DST,shrinkToFit:!1}),i=new xe({data:Ph,label:"index-batch-buffer",usage:J.INDEX|J.COPY_DST,shrinkToFit:!1}),r=6*4;super({attributes:{aPosition:{buffer:e,format:"float32x2",stride:r,offset:0},aUV:{buffer:e,format:"float32x2",stride:r,offset:2*4},aColor:{buffer:e,format:"unorm8x4",stride:r,offset:4*4},aTextureIdAndRound:{buffer:e,format:"uint16x2",stride:r,offset:5*4}},indexBuffer:i})}}function $i(s,t,e){if(s)for(const i in s){const r=i.toLocaleLowerCase(),n=t[r];if(n){let o=s[i];i==="header"&&(o=o.replace(/@in\s+[^;]+;\s*/g,"").replace(/@out\s+[^;]+;\s*/g,"")),e&&n.push(`//----${e}----//`),n.push(o)}else Q(`${i} placement hook does not exist in shader`)}}const Ch=/\{\{(.*?)\}\}/g;function Oi(s){var i;const t={};return(((i=s.match(Ch))==null?void 0:i.map(r=>r.replace(/[{()}]/g,"")))??[]).forEach(r=>{t[r]=[]}),t}function Vi(s,t){let e;const i=/@in\s+([^;]+);/g;for(;(e=i.exec(s))!==null;)t.push(e[1])}function Wi(s,t,e=!1){const i=[];Vi(t,i),s.forEach(a=>{a.header&&Vi(a.header,i)});const r=i;e&&r.sort();const n=r.map((a,h)=>` @location(${h}) ${a},`).join(` +`);let o=t.replace(/@in\s+[^;]+;\s*/g,"");return o=o.replace("{{in}}",` +${n} +`),o}function Yi(s,t){let e;const i=/@out\s+([^;]+);/g;for(;(e=i.exec(s))!==null;)t.push(e[1])}function Th(s){const e=/\b(\w+)\s*:/g.exec(s);return e?e[1]:""}function Ah(s){const t=/@.*?\s+/g;return s.replace(t,"")}function kh(s,t){const e=[];Yi(t,e),s.forEach(h=>{h.header&&Yi(h.header,e)});let i=0;const r=e.sort().map(h=>h.indexOf("builtin")>-1?h:`@location(${i++}) ${h}`).join(`, +`),n=e.sort().map(h=>` var ${Ah(h)};`).join(` +`),o=`return VSOutput( + ${e.sort().map(h=>` ${Th(h)}`).join(`, +`)});`;let a=t.replace(/@out\s+[^;]+;\s*/g,"");return a=a.replace("{{struct}}",` +${r} +`),a=a.replace("{{start}}",` +${n} +`),a=a.replace("{{return}}",` +${o} +`),a}function Xi(s,t){let e=s;for(const i in t){const r=t[i];r.join(` +`).length?e=e.replace(`{{${i}}}`,`//-----${i} START-----// +${r.join(` +`)} +//----${i} FINISH----//`):e=e.replace(`{{${i}}}`,"")}return e}const St=Object.create(null),ds=new Map;let Eh=0;function Rh({template:s,bits:t}){const e=Tn(s,t);if(St[e])return St[e];const{vertex:i,fragment:r}=Bh(s,t);return St[e]=An(i,r,t),St[e]}function Ih({template:s,bits:t}){const e=Tn(s,t);return St[e]||(St[e]=An(s.vertex,s.fragment,t)),St[e]}function Bh(s,t){const e=t.map(o=>o.vertex).filter(o=>!!o),i=t.map(o=>o.fragment).filter(o=>!!o);let r=Wi(e,s.vertex,!0);r=kh(e,r);const n=Wi(i,s.fragment,!0);return{vertex:r,fragment:n}}function Tn(s,t){return t.map(e=>(ds.has(e)||ds.set(e,Eh++),ds.get(e))).sort((e,i)=>e-i).join("-")+s.vertex+s.fragment}function An(s,t,e){const i=Oi(s),r=Oi(t);return e.forEach(n=>{$i(n.vertex,i,n.name),$i(n.fragment,r,n.name)}),{vertex:Xi(s,i),fragment:Xi(t,r)}}const Fh=` + @in aPosition: vec2; + @in aUV: vec2; + + @out @builtin(position) vPosition: vec4; + @out vUV : vec2; + @out vColor : vec4; + + {{header}} + + struct VSOutput { + {{struct}} + }; + + @vertex + fn main( {{in}} ) -> VSOutput { + + var worldTransformMatrix = globalUniforms.uWorldTransformMatrix; + var modelMatrix = mat3x3( + 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0 + ); + var position = aPosition; + var uv = aUV; + + {{start}} + + vColor = vec4(1., 1., 1., 1.); + + {{main}} + + vUV = uv; + + var modelViewProjectionMatrix = globalUniforms.uProjectionMatrix * worldTransformMatrix * modelMatrix; + + vPosition = vec4((modelViewProjectionMatrix * vec3(position, 1.0)).xy, 0.0, 1.0); + + vColor *= globalUniforms.uWorldColorAlpha; + + {{end}} + + {{return}} + }; +`,Gh=` + @in vUV : vec2; + @in vColor : vec4; + + {{header}} + + @fragment + fn main( + {{in}} + ) -> @location(0) vec4 { + + {{start}} + + var outColor:vec4; + + {{main}} + + var finalColor:vec4 = outColor * vColor; + + {{end}} + + return finalColor; + }; +`,Lh=` + in vec2 aPosition; + in vec2 aUV; + + out vec4 vColor; + out vec2 vUV; + + {{header}} + + void main(void){ + + mat3 worldTransformMatrix = uWorldTransformMatrix; + mat3 modelMatrix = mat3( + 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0 + ); + vec2 position = aPosition; + vec2 uv = aUV; + + {{start}} + + vColor = vec4(1.); + + {{main}} + + vUV = uv; + + mat3 modelViewProjectionMatrix = uProjectionMatrix * worldTransformMatrix * modelMatrix; + + gl_Position = vec4((modelViewProjectionMatrix * vec3(position, 1.0)).xy, 0.0, 1.0); + + vColor *= uWorldColorAlpha; + + {{end}} + } +`,Dh=` + + in vec4 vColor; + in vec2 vUV; + + out vec4 finalColor; + + {{header}} + + void main(void) { + + {{start}} + + vec4 outColor; + + {{main}} + + finalColor = outColor * vColor; + + {{end}} + } +`,zh={name:"global-uniforms-bit",vertex:{header:` + struct GlobalUniforms { + uProjectionMatrix:mat3x3, + uWorldTransformMatrix:mat3x3, + uWorldColorAlpha: vec4, + uResolution: vec2, + } + + @group(0) @binding(0) var globalUniforms : GlobalUniforms; + `}},Hh={name:"global-uniforms-bit",vertex:{header:` + uniform mat3 uProjectionMatrix; + uniform mat3 uWorldTransformMatrix; + uniform vec4 uWorldColorAlpha; + uniform vec2 uResolution; + `}};function Nh({bits:s,name:t}){const e=Rh({template:{fragment:Gh,vertex:Fh},bits:[zh,...s]});return Ye.from({name:t,vertex:{source:e.vertex,entryPoint:"main"},fragment:{source:e.fragment,entryPoint:"main"}})}function Uh({bits:s,name:t}){return new nn({name:t,...Ih({template:{vertex:Lh,fragment:Dh},bits:[Hh,...s]})})}const $h={name:"color-bit",vertex:{header:` + @in aColor: vec4; + `,main:` + vColor *= vec4(aColor.rgb * aColor.a, aColor.a); + `}},Oh={name:"color-bit",vertex:{header:` + in vec4 aColor; + `,main:` + vColor *= vec4(aColor.rgb * aColor.a, aColor.a); + `}},fs={};function Vh(s){const t=[];if(s===1)t.push("@group(1) @binding(0) var textureSource1: texture_2d;"),t.push("@group(1) @binding(1) var textureSampler1: sampler;");else{let e=0;for(let i=0;i;`),t.push(`@group(1) @binding(${e++}) var textureSampler${i+1}: sampler;`)}return t.join(` +`)}function Wh(s){const t=[];if(s===1)t.push("outColor = textureSampleGrad(textureSource1, textureSampler1, vUV, uvDx, uvDy);");else{t.push("switch vTextureId {");for(let e=0;e; + @out @interpolate(flat) vTextureId : u32; + `,main:` + vTextureId = aTextureIdAndRound.y; + `,end:` + if(aTextureIdAndRound.x == 1) + { + vPosition = vec4(roundPixels(vPosition.xy, globalUniforms.uResolution), vPosition.zw); + } + `},fragment:{header:` + @in @interpolate(flat) vTextureId: u32; + + ${Vh(s)} + `,main:` + var uvDx = dpdx(vUV); + var uvDy = dpdy(vUV); + + ${Wh(s)} + `}}),fs[s]}const ps={};function Xh(s){const t=[];for(let e=0;e0&&t.push("else"),e, targetSize: vec2) -> vec2 + { + return (floor(((position * 0.5 + 0.5) * targetSize) + 0.5) / targetSize) * 2.0 - 1.0; + } + `}},Zh={name:"round-pixels-bit",vertex:{header:` + vec2 roundPixels(vec2 position, vec2 targetSize) + { + return (floor(((position * 0.5 + 0.5) * targetSize) + 0.5) / targetSize) * 2.0 - 1.0; + } + `}},ji={};function Kh(s){let t=ji[s];if(t)return t;const e=new Int32Array(s);for(let i=0;i>16|t&65280|(t&255)<<16,i=this.renderable;return i?Gr(e,i.groupColor)+(this.alpha*i.groupAlpha*255<<24):e+(this.alpha*255<<24)}get transform(){var t;return((t=this.renderable)==null?void 0:t.groupTransform)||sl}copyTo(t){t.indexOffset=this.indexOffset,t.indexSize=this.indexSize,t.attributeOffset=this.attributeOffset,t.attributeSize=this.attributeSize,t.baseColor=this.baseColor,t.alpha=this.alpha,t.texture=this.texture,t.geometryData=this.geometryData,t.topology=this.topology}reset(){this.applyTransform=!0,this.renderable=null,this.topology="triangle-list"}destroy(){this.renderable=null,this.texture=null,this.geometryData=null,this._batcher=null,this._batch=null}}const ye={extension:{type:L.ShapeBuilder,name:"circle"},build(s,t){let e,i,r,n,o,a;if(s.type==="circle"){const b=s;if(o=a=b.radius,o<=0)return!1;e=b.x,i=b.y,r=n=0}else if(s.type==="ellipse"){const b=s;if(o=b.halfWidth,a=b.halfHeight,o<=0||a<=0)return!1;e=b.x,i=b.y,r=n=0}else{const b=s,v=b.width/2,S=b.height/2;e=b.x+v,i=b.y+S,o=a=Math.max(0,Math.min(b.radius,Math.min(v,S))),r=v-o,n=S-a}if(r<0||n<0)return!1;const h=Math.ceil(2.3*Math.sqrt(o+a)),l=h*8+(r?4:0)+(n?4:0);if(l===0)return!1;if(h===0)return t[0]=t[6]=e+r,t[1]=t[3]=i+n,t[2]=t[4]=e-r,t[5]=t[7]=i-n,!0;let c=0,u=h*4+(r?2:0)+2,p=u,d=l,m=r+o,g=n,f=e+m,x=e-m,y=i+g;if(t[c++]=f,t[c++]=y,t[--u]=y,t[--u]=x,n){const b=i-g;t[p++]=x,t[p++]=b,t[--d]=b,t[--d]=f}for(let b=1;b0&&(r[n++]=h,r[n++]=l,r[n++]=h-1),h++;r[n++]=l+1,r[n++]=l,r[n++]=h-1}},il={...ye,extension:{...ye.extension,name:"ellipse"}},rl={...ye,extension:{...ye.extension,name:"roundedRectangle"}},Bn=1e-4,qi=1e-4;function nl(s){const t=s.length;if(t<6)return 1;let e=0;for(let i=0,r=s[t-2],n=s[t-1];iu&&(u+=Math.PI*2);let p=c;const d=u-c,m=Math.abs(d),g=Math.sqrt(h*h+l*l),f=(15*m*Math.sqrt(g)/Math.PI>>0)+1,x=d/f;if(p+=x,a){o.push(s,t),o.push(e,i);for(let y=1,_=p;y=0&&(a.join==="round"?g+=Tt(S,w,S-P*C,w-M*C,S-G*C,w-E*C,d,!1)+4:g+=2,d.push(S-G*A,w-E*A),d.push(S+G*C,w+E*C));continue}const si=(-P+b)*(-M+w)-(-P+S)*(-M+v),ii=(-G+R)*(-E+w)-(-G+S)*(-E+k),we=(wt*ii-qt*si)/be,Se=(Zt*si-jt*ii)/be,qe=(we-S)*(we-S)+(Se-w)*(Se-w),vt=S+(we-S)*C,Pt=w+(Se-w)*C,Mt=S-(we-S)*A,Ct=w-(Se-w)*A,On=Math.min(wt*wt+jt*jt,qt*qt+Zt*Zt),ri=Kt?C:A,Vn=On+ri*ri*y;qe<=Vn?a.join==="bevel"||qe/y>_?(Kt?(d.push(vt,Pt),d.push(S+P*A,w+M*A),d.push(vt,Pt),d.push(S+G*A,w+E*A)):(d.push(S-P*C,w-M*C),d.push(Mt,Ct),d.push(S-G*C,w-E*C),d.push(Mt,Ct)),g+=2):a.join==="round"?Kt?(d.push(vt,Pt),d.push(S+P*A,w+M*A),g+=Tt(S,w,S+P*A,w+M*A,S+G*A,w+E*A,d,!0)+4,d.push(vt,Pt),d.push(S+G*A,w+E*A)):(d.push(S-P*C,w-M*C),d.push(Mt,Ct),g+=Tt(S,w,S-P*C,w-M*C,S-G*C,w-E*C,d,!1)+4,d.push(S-G*C,w-E*C),d.push(Mt,Ct)):(d.push(vt,Pt),d.push(Mt,Ct)):(d.push(S-P*C,w-M*C),d.push(S+P*A,w+M*A),a.join==="round"?Kt?g+=Tt(S,w,S+P*A,w+M*A,S+G*A,w+E*A,d,!0)+2:g+=Tt(S,w,S-P*C,w-M*C,S-G*C,w-E*C,d,!1)+2:a.join==="miter"&&qe/y<=_&&(Kt?(d.push(Mt,Ct),d.push(Mt,Ct)):(d.push(vt,Pt),d.push(vt,Pt)),g+=2),d.push(S-G*C,w-E*C),d.push(S+G*A,w+E*A),g+=2)}b=s[(m-2)*2],v=s[(m-2)*2+1],S=s[(m-1)*2],w=s[(m-1)*2+1],P=-(v-w),M=b-S,N=Math.sqrt(P*P+M*M),P/=N,M/=N,P*=x,M*=x,d.push(S-P*C,w-M*C),d.push(S+P*A,w+M*A),u||(a.cap==="round"?g+=Tt(S-P*(C-A)*.5,w-M*(C-A)*.5,S-P*C,w-M*C,S+P*A,w+M*A,d,!1)+2:a.cap==="square"&&(g+=Zi(S,w,P,M,C,A,!1,d)));const Xt=qi*qi;for(let B=f;B0&&o>0?(t[0]=i,t[1]=r,t[2]=i+n,t[3]=r,t[4]=i+n,t[5]=r+o,t[6]=i,t[7]=r+o,!0):!1},triangulate(s,t,e,i,r,n){let o=0;i*=e,t[i+o]=s[0],t[i+o+1]=s[1],o+=e,t[i+o]=s[2],t[i+o+1]=s[3],o+=e,t[i+o]=s[6],t[i+o+1]=s[7],o+=e,t[i+o]=s[4],t[i+o+1]=s[5],o+=e;const a=i/e;r[n++]=a,r[n++]=a+1,r[n++]=a+2,r[n++]=a+1,r[n++]=a+3,r[n++]=a+2}},ul={extension:{type:L.ShapeBuilder,name:"triangle"},build(s,t){return t[0]=s.x,t[1]=s.y,t[2]=s.x2,t[3]=s.y2,t[4]=s.x3,t[5]=s.y3,!0},triangulate(s,t,e,i,r,n){let o=0;i*=e,t[i+o]=s[0],t[i+o+1]=s[1],o+=e,t[i+o]=s[2],t[i+o+1]=s[3],o+=e,t[i+o]=s[4],t[i+o+1]=s[5];const a=i/e;r[n++]=a,r[n++]=a+1,r[n++]=a+2}},dl=new I,fl=new W;function pl(s,t,e,i){const r=t.matrix?s.copyFrom(t.matrix).invert():s.identity();if(t.textureSpace==="local"){const o=e.getBounds(fl);t.width&&o.pad(t.width);const{x:a,y:h}=o,l=1/o.width,c=1/o.height,u=-a*l,p=-h*c,d=r.a,m=r.b,g=r.c,f=r.d;r.a*=l,r.b*=l,r.c*=c,r.d*=c,r.tx=u*d+p*g+r.tx,r.ty=u*m+p*f+r.ty}else r.translate(t.texture.frame.x,t.texture.frame.y),r.scale(1/t.texture.source.width,1/t.texture.source.height);const n=t.texture.source.style;return!(t.fill instanceof _t)&&n.addressMode==="clamp-to-edge"&&(n.addressMode="repeat",n.update()),i&&r.append(dl.copyFrom(i).invert()),r}const je={};rt.handleByMap(L.ShapeBuilder,je);rt.add(cl,ll,ul,ye,il,rl);const ml=new W,gl=new I;function xl(s,t){const{geometryData:e,batches:i}=t;i.length=0,e.indices.length=0,e.vertices.length=0,e.uvs.length=0;for(let r=0;r{const u=[],p=je[h.type];if(!p.build(h,u))return;const d=a.length,m=n.length/2;let g="triangle-list";if(l&&Rn(u,l),e){const _=h.closePath??!0,b=t;b.pixelLine?(al(u,_,n,a),g="line-list"):ol(u,b,!1,_,n,a)}else if(c){const _=[],b=u.slice();_l(c).forEach(S=>{_.push(b.length/2),b.push(...S)}),Fn(b,_,n,2,m,a,d)}else p.triangulate(u,n,2,m,a,d);const f=o.length/2,x=t.texture;if(x!==D.WHITE){const _=pl(gl,t,h,l);tl(n,2,m,o,f,2,n.length/2-m,_)}else el(o,f,2,n.length/2-m);const y=it.get(In);y.indexOffset=d,y.indexSize=a.length-d,y.attributeOffset=m,y.attributeSize=n.length/2-m,y.baseColor=t.color,y.alpha=t.alpha,y.texture=x,y.geometryData=r,y.topology=g,i.push(y)})}function _l(s){const t=[];for(let e=0;e{it.return(i)})}destroy(){for(const t in this._gpuContextHash)this._gpuContextHash[t]&&this.onGraphicsContextDestroy(this._gpuContextHash[t].context)}};Ks.extension={type:[L.WebGLSystem,L.WebGPUSystem,L.CanvasSystem],name:"graphicsContext"};Ks.defaultOptions={bezierSmoothness:.5};let Gn=Ks;const Sl=8,Le=11920929e-14,vl=1;function Ln(s,t,e,i,r,n,o,a,h,l){const u=Math.min(.99,Math.max(0,l??Gn.defaultOptions.bezierSmoothness));let p=(vl-u)/1;return p*=p,Pl(t,e,i,r,n,o,a,h,s,p),s}function Pl(s,t,e,i,r,n,o,a,h,l){Bs(s,t,e,i,r,n,o,a,h,l,0),h.push(o,a)}function Bs(s,t,e,i,r,n,o,a,h,l,c){if(c>Sl)return;const u=(s+e)/2,p=(t+i)/2,d=(e+r)/2,m=(i+n)/2,g=(r+o)/2,f=(n+a)/2,x=(u+d)/2,y=(p+m)/2,_=(d+g)/2,b=(m+f)/2,v=(x+_)/2,S=(y+b)/2;if(c>0){let w=o-s,R=a-t;const k=Math.abs((e-o)*R-(i-a)*w),P=Math.abs((r-o)*R-(n-a)*w);if(k>Le&&P>Le){if((k+P)*(k+P)<=l*(w*w+R*R)){h.push(v,S);return}}else if(k>Le){if(k*k<=l*(w*w+R*R)){h.push(v,S);return}}else if(P>Le){if(P*P<=l*(w*w+R*R)){h.push(v,S);return}}else if(w=v-(s+o)/2,R=S-(t+a)/2,w*w+R*R<=l){h.push(v,S);return}}Bs(s,t,u,p,x,y,v,S,h,l,c+1),Bs(v,S,_,b,g,f,o,a,h,l,c+1)}const Ml=8,Cl=11920929e-14,Tl=1;function Al(s,t,e,i,r,n,o,a){const l=Math.min(.99,Math.max(0,a??Gn.defaultOptions.bezierSmoothness));let c=(Tl-l)/1;return c*=c,kl(t,e,i,r,n,o,s,c),s}function kl(s,t,e,i,r,n,o,a){Fs(o,s,t,e,i,r,n,a,0),o.push(r,n)}function Fs(s,t,e,i,r,n,o,a,h){if(h>Ml)return;const l=(t+i)/2,c=(e+r)/2,u=(i+n)/2,p=(r+o)/2,d=(l+u)/2,m=(c+p)/2;let g=n-t,f=o-e;const x=Math.abs((i-n)*f-(r-o)*g);if(x>Cl){if(x*x<=a*(g*g+f*f)){s.push(d,m);return}}else if(g=d-(t+n)/2,f=m-(e+o)/2,g*g+f*f<=a){s.push(d,m);return}Fs(s,t,e,l,c,d,m,a,h+1),Fs(s,d,m,u,p,n,o,a,h+1)}function Dn(s,t,e,i,r,n,o,a){let h=Math.abs(r-n);(!o&&r>n||o&&n>r)&&(h=2*Math.PI-h),a||(a=Math.max(6,Math.floor(6*Math.pow(i,1/3)*(h/Math.PI)))),a=Math.max(a,3);let l=h/a,c=r;l*=o?-1:1;for(let u=0;uu*h)}const ue=Math.PI*2,gs={centerX:0,centerY:0,ang1:0,ang2:0},xs=({x:s,y:t},e,i,r,n,o,a,h)=>{s*=e,t*=i;const l=r*s-n*t,c=n*s+r*t;return h.x=l+o,h.y=c+a,h};function Rl(s,t){const e=t===-1.5707963267948966?-.551915024494:1.3333333333333333*Math.tan(t/4),i=t===1.5707963267948966?.551915024494:e,r=Math.cos(s),n=Math.sin(s),o=Math.cos(s+t),a=Math.sin(s+t);return[{x:r-n*i,y:n+r*i},{x:o+a*i,y:a-o*i},{x:o,y:a}]}const Qi=(s,t,e,i)=>{const r=s*i-t*e<0?-1:1;let n=s*e+t*i;return n>1&&(n=1),n<-1&&(n=-1),r*Math.acos(n)},Il=(s,t,e,i,r,n,o,a,h,l,c,u,p)=>{const d=Math.pow(r,2),m=Math.pow(n,2),g=Math.pow(c,2),f=Math.pow(u,2);let x=d*m-d*f-m*g;x<0&&(x=0),x/=d*f+m*g,x=Math.sqrt(x)*(o===a?-1:1);const y=x*r/n*u,_=x*-n/r*c,b=l*y-h*_+(s+e)/2,v=h*y+l*_+(t+i)/2,S=(c-y)/r,w=(u-_)/n,R=(-c-y)/r,k=(-u-_)/n,P=Qi(1,0,S,w);let M=Qi(S,w,R,k);a===0&&M>0&&(M-=ue),a===1&&M<0&&(M+=ue),p.centerX=b,p.centerY=v,p.ang1=P,p.ang2=M};function Bl(s,t,e,i,r,n,o,a=0,h=0,l=0){if(n===0||o===0)return;const c=Math.sin(a*ue/360),u=Math.cos(a*ue/360),p=u*(t-i)/2+c*(e-r)/2,d=-c*(t-i)/2+u*(e-r)/2;if(p===0&&d===0)return;n=Math.abs(n),o=Math.abs(o);const m=Math.pow(p,2)/Math.pow(n,2)+Math.pow(d,2)/Math.pow(o,2);m>1&&(n*=Math.sqrt(m),o*=Math.sqrt(m)),Il(t,e,i,r,n,o,h,l,c,u,p,d,gs);let{ang1:g,ang2:f}=gs;const{centerX:x,centerY:y}=gs;let _=Math.abs(f)/(ue/4);Math.abs(1-_)<1e-7&&(_=1);const b=Math.max(Math.ceil(_),1);f/=b;let v=s[s.length-2],S=s[s.length-1];const w={x:0,y:0};for(let R=0;R{const h=a.x-o.x,l=a.y-o.y,c=Math.sqrt(h*h+l*l),u=h/c,p=l/c;return{len:c,nx:u,ny:p}},r=(o,a)=>{o===0?s.moveTo(a.x,a.y):s.lineTo(a.x,a.y)};let n=t[t.length-1];for(let o=0;o0&&(d=-1,m=!0);const g=p/2;let f,x=Math.abs(Math.cos(g)*h/Math.sin(g));x>Math.min(c.len/2,u.len/2)?(x=Math.min(c.len/2,u.len/2),f=Math.abs(x*Math.sin(g)/Math.cos(g))):f=h;const y=a.x+u.nx*x+-u.ny*f*d,_=a.y+u.ny*x+u.nx*f*d,b=Math.atan2(c.ny,c.nx)+Math.PI/2*d,v=Math.atan2(u.ny,u.nx)-Math.PI/2*d;o===0&&s.moveTo(y+Math.cos(b)*f,_+Math.sin(b)*f),s.arc(y,_,f,b,v,m),n=a}}function Gl(s,t,e,i){const r=(a,h)=>Math.sqrt((a.x-h.x)**2+(a.y-h.y)**2),n=(a,h,l)=>({x:a.x+(h.x-a.x)*l,y:a.y+(h.y-a.y)*l}),o=t.length;for(let a=0;a1){let n=null;for(let o=r;o=2;u-=2)c[u]===c[u-2]&&c[u-1]===c[u-3]&&c.splice(u-1,2);return this.poly(c,!0,o)}ellipse(t,e,i,r,n){return this.drawShape(new qs(t,e,i,r),n),this}roundRect(t,e,i,r,n,o){return this.drawShape(new Zs(t,e,i,r,n),o),this}drawShape(t,e){return this.endPoly(),this.shapePrimitives.push({shape:t,transform:e}),this}startPoly(t,e){let i=this._currentPoly;return i&&this.endPoly(),i=new le,i.points.push(t,e),this._currentPoly=i,this}endPoly(t=!1){const e=this._currentPoly;return e&&e.points.length>2&&(e.closePath=t,this.shapePrimitives.push({shape:e})),this._currentPoly=null,this}_ensurePoly(t=!0){if(!this._currentPoly&&(this._currentPoly=new le,t)){const e=this.shapePrimitives[this.shapePrimitives.length-1];if(e){let i=e.shape.x,r=e.shape.y;if(e.transform&&!e.transform.isIdentity()){const n=e.transform,o=i;i=n.a*i+n.c*r+n.tx,r=n.b*o+n.d*r+n.ty}this._currentPoly.points.push(i,r)}else this._currentPoly.points.push(0,0)}}buildPath(){const t=this._graphicsPath2D;this.shapePrimitives.length=0,this._currentPoly=null;for(let e=0;ea.area).sort((a,h)=>h-a),[e,i]=t,r=t[t.length-1],n=e/i,o=i/r;return!(n>3&&o<2)}function $l(s){return s.split(/(?=[Mm])/).filter(i=>i.trim().length>0)}function Ol(s){const t=s.match(/[-+]?[0-9]*\.?[0-9]+/g);if(!t||t.length<4)return 0;const e=t.map(Number),i=[],r=[];for(let c=0;c1;if(M&&G){const N=P.map(C=>({path:C,area:Ol(C)}));if(N.sort((C,A)=>A.area-C.area),P.length>3||!Ul(N))for(let C=0;CparseInt(k,10)),t.context.poly(_,!0),e&&t.context.fill(e),i&&t.context.stroke(i);break;case"polyline":b=s.getAttribute("points"),_=b.match(/\d+/g).map(k=>parseInt(k,10)),t.context.poly(_,!1),i&&t.context.stroke(i);break;case"g":case"svg":break;default:{Q(`[SVG parser] <${s.nodeName}> elements unsupported`);break}}a&&(e=null);for(let k=0;k{this._fill=Bt({...this._originalFill},ot.defaultFillStyle)})),this._fill=Bt(t===0?"black":t,ot.defaultFillStyle),this.update())}get stroke(){return this._originalStroke}set stroke(t){t!==this._originalStroke&&(this._originalStroke=t,this._isFillStyle(t)&&(this._originalStroke=this._createProxy({...ot.defaultStrokeStyle,...t},()=>{this._stroke=Ve({...this._originalStroke},ot.defaultStrokeStyle)})),this._stroke=Ve(t,ot.defaultStrokeStyle),this.update())}update(){this._tick++,this.emit("update",this)}reset(){const t=Ut.defaultTextStyle;for(const e in t)this[e]=t[e]}get styleKey(){return`${this.uid}-${this._tick}`}clone(){return new Ut({align:this.align,breakWords:this.breakWords,dropShadow:this._dropShadow?{...this._dropShadow}:null,fill:this._fill,fontFamily:this.fontFamily,fontSize:this.fontSize,fontStyle:this.fontStyle,fontVariant:this.fontVariant,fontWeight:this.fontWeight,leading:this.leading,letterSpacing:this.letterSpacing,lineHeight:this.lineHeight,padding:this.padding,stroke:this._stroke,textBaseline:this.textBaseline,whiteSpace:this.whiteSpace,wordWrap:this.wordWrap,wordWrapWidth:this.wordWrapWidth,filters:this._filters?[...this._filters]:void 0})}_getFinalPadding(){let t=0;if(this._filters)for(let e=0;e(i[r]=n,e==null||e(r,n),this.update(),!0)})}_isFillStyle(t){return(t??null)!==null&&!(Y.isColorLike(t)||t instanceof _t||t instanceof Xe)}};Js.defaultDropShadow={alpha:1,angle:Math.PI/6,blur:0,color:"black",distance:5};Js.defaultTextStyle={align:"left",breakWords:!1,dropShadow:null,fill:"black",fontFamily:"Arial",fontSize:26,fontStyle:"normal",fontVariant:"normal",fontWeight:"normal",leading:0,letterSpacing:0,lineHeight:0,padding:0,stroke:null,textBaseline:"alphabetic",trim:!1,whiteSpace:"pre",wordWrap:!1,wordWrapWidth:100};let Nn=Js;function Kl(s){const t=s;if(typeof t.dropShadow=="boolean"&&t.dropShadow){const e=Nn.defaultDropShadow;s.dropShadow={alpha:t.dropShadowAlpha??e.alpha,angle:t.dropShadowAngle??e.angle,blur:t.dropShadowBlur??e.blur,color:t.dropShadowColor??e.color,distance:t.dropShadowDistance??e.distance}}if(t.strokeThickness!==void 0){F(O,"strokeThickness is now a part of stroke");const e=t.stroke;let i={};if(Y.isColorLike(e))i.color=e;else if(e instanceof _t||e instanceof Xe)i.fill=e;else if(Object.hasOwnProperty.call(e,"color")||Object.hasOwnProperty.call(e,"fill"))i=e;else throw new Error("Invalid stroke value.");s.stroke={...i,width:t.strokeThickness}}if(Array.isArray(t.fillGradientStops)){if(F(O,"gradient fill is now a fill pattern: `new FillGradient(...)`"),!Array.isArray(t.fill)||t.fill.length===0)throw new Error("Invalid fill value. Expected an array of colors for gradient fill.");t.fill.length!==t.fillGradientStops.length&&Q("The number of fill colors must match the number of fill gradient stops.");const e=new _t({start:{x:0,y:0},end:{x:0,y:1},textureSpace:"local"}),i=t.fillGradientStops.slice(),r=t.fill.map(n=>Y.shared.setValue(n).toNumber());i.forEach((n,o)=>{e.addColorStop(n,r[o])}),s.fill={fill:e}}}class Ql{constructor(t){this._canvasPool=Object.create(null),this.canvasOptions=t||{},this.enableFullScreen=!1}_createCanvasAndContext(t,e){const i=et.get().createCanvas();i.width=t,i.height=e;const r=i.getContext("2d");return{canvas:i,context:r}}getOptimalCanvasAndContext(t,e,i=1){t=Math.ceil(t*i-1e-6),e=Math.ceil(e*i-1e-6),t=Vt(t),e=Vt(e);const r=(t<<17)+(e<<1);this._canvasPool[r]||(this._canvasPool[r]=[]);let n=this._canvasPool[r].pop();return n||(n=this._createCanvasAndContext(t,e)),n}returnCanvasAndContext(t){const e=t.canvas,{width:i,height:r}=e,n=(i<<17)+(r<<1);t.context.resetTransform(),t.context.clearRect(0,0,i,r),this._canvasPool[n].push(t)}clear(){this._canvasPool={}}}const Gs=new Ql;_e.register(Gs);const hr=1e5;function lr(s,t,e,i=0){if(s.texture===D.WHITE&&!s.fill)return Y.shared.setValue(s.color).setAlpha(s.alpha??1).toHexa();if(s.fill){if(s.fill instanceof Xe){const r=s.fill,n=t.createPattern(r.texture.source.resource,"repeat"),o=r.transform.copyTo(I.shared);return o.scale(r.texture.frame.width,r.texture.frame.height),n.setTransform(o),n}else if(s.fill instanceof _t){const r=s.fill,n=r.type==="linear",o=r.textureSpace==="local";let a=1,h=1;o&&e&&(a=e.width+i,h=e.height+i);let l,c=!1;if(n){const{start:u,end:p}=r;l=t.createLinearGradient(u.x*a,u.y*h,p.x*a,p.y*h),c=Math.abs(p.x-u.x){const g=d+m.offset*u;l.addColorStop(Math.floor(g*hr)/hr,Y.shared.setValue(m.color).toHex())})}}else r.colorStops.forEach(u=>{l.addColorStop(u.offset,Y.shared.setValue(u.color).toHex())});return l}}else{const r=t.createPattern(s.texture.source.resource,"repeat"),n=s.matrix.copyTo(I.shared);return n.scale(s.texture.frame.width,s.texture.frame.height),r.setTransform(n),r}return Q("FillStyle not recognised",s),"red"}class dt extends Vs{constructor(t){t instanceof ot&&(t={context:t});const{context:e,roundPixels:i,...r}=t||{};super({label:"Graphics",...r}),this.renderPipeId="graphics",e?this._context=e:this._context=this._ownedContext=new ot,this._context.on("update",this.onViewUpdate,this),this.didViewUpdate=!0,this.allowChildren=!1,this.roundPixels=i??!1}set context(t){t!==this._context&&(this._context.off("update",this.onViewUpdate,this),this._context=t,this._context.on("update",this.onViewUpdate,this),this.onViewUpdate())}get context(){return this._context}get bounds(){return this._context.bounds}updateBounds(){}containsPoint(t){return this._context.containsPoint(t)}destroy(t){this._ownedContext&&!t?this._ownedContext.destroy(t):(t===!0||(t==null?void 0:t.context)===!0)&&this._context.destroy(t),this._ownedContext=null,this._context=null,super.destroy(t)}_callContextMethod(t,e){return this.context[t](...e),this}setFillStyle(...t){return this._callContextMethod("setFillStyle",t)}setStrokeStyle(...t){return this._callContextMethod("setStrokeStyle",t)}fill(...t){return this._callContextMethod("fill",t)}stroke(...t){return this._callContextMethod("stroke",t)}texture(...t){return this._callContextMethod("texture",t)}beginPath(){return this._callContextMethod("beginPath",[])}cut(){return this._callContextMethod("cut",[])}arc(...t){return this._callContextMethod("arc",t)}arcTo(...t){return this._callContextMethod("arcTo",t)}arcToSvg(...t){return this._callContextMethod("arcToSvg",t)}bezierCurveTo(...t){return this._callContextMethod("bezierCurveTo",t)}closePath(){return this._callContextMethod("closePath",[])}ellipse(...t){return this._callContextMethod("ellipse",t)}circle(...t){return this._callContextMethod("circle",t)}path(...t){return this._callContextMethod("path",t)}lineTo(...t){return this._callContextMethod("lineTo",t)}moveTo(...t){return this._callContextMethod("moveTo",t)}quadraticCurveTo(...t){return this._callContextMethod("quadraticCurveTo",t)}rect(...t){return this._callContextMethod("rect",t)}roundRect(...t){return this._callContextMethod("roundRect",t)}poly(...t){return this._callContextMethod("poly",t)}regularPoly(...t){return this._callContextMethod("regularPoly",t)}roundPoly(...t){return this._callContextMethod("roundPoly",t)}roundShape(...t){return this._callContextMethod("roundShape",t)}filletRect(...t){return this._callContextMethod("filletRect",t)}chamferRect(...t){return this._callContextMethod("chamferRect",t)}star(...t){return this._callContextMethod("star",t)}svg(...t){return this._callContextMethod("svg",t)}restore(...t){return this._callContextMethod("restore",t)}save(){return this._callContextMethod("save",[])}getTransform(){return this.context.getTransform()}resetTransform(){return this._callContextMethod("resetTransform",[])}rotateTransform(...t){return this._callContextMethod("rotate",t)}scaleTransform(...t){return this._callContextMethod("scale",t)}setTransform(...t){return this._callContextMethod("setTransform",t)}transform(...t){return this._callContextMethod("transform",t)}translateTransform(...t){return this._callContextMethod("translate",t)}clear(){return this._callContextMethod("clear",[])}get fillStyle(){return this._context.fillStyle}set fillStyle(t){this._context.fillStyle=t}get strokeStyle(){return this._context.strokeStyle}set strokeStyle(t){this._context.strokeStyle=t}clone(t=!1){return t?new dt(this._context.clone()):(this._ownedContext=null,new dt(this._context))}lineStyle(t,e,i){F(O,"Graphics#lineStyle is no longer needed. Use Graphics#setStrokeStyle to set the stroke style.");const r={};return t&&(r.width=t),e&&(r.color=e),i&&(r.alpha=i),this.context.strokeStyle=r,this}beginFill(t,e){F(O,"Graphics#beginFill is no longer needed. Use Graphics#fill to fill the shape with the desired style.");const i={};return t!==void 0&&(i.color=t),e!==void 0&&(i.alpha=e),this.context.fillStyle=i,this}endFill(){F(O,"Graphics#endFill is no longer needed. Use Graphics#fill to fill the shape with the desired style."),this.context.fill();const t=this.context.strokeStyle;return(t.width!==ot.defaultStrokeStyle.width||t.color!==ot.defaultStrokeStyle.color||t.alpha!==ot.defaultStrokeStyle.alpha)&&this.context.stroke(),this}drawCircle(...t){return F(O,"Graphics#drawCircle has been renamed to Graphics#circle"),this._callContextMethod("circle",t)}drawEllipse(...t){return F(O,"Graphics#drawEllipse has been renamed to Graphics#ellipse"),this._callContextMethod("ellipse",t)}drawPolygon(...t){return F(O,"Graphics#drawPolygon has been renamed to Graphics#poly"),this._callContextMethod("poly",t)}drawRect(...t){return F(O,"Graphics#drawRect has been renamed to Graphics#rect"),this._callContextMethod("rect",t)}drawRoundedRect(...t){return F(O,"Graphics#drawRoundedRect has been renamed to Graphics#roundRect"),this._callContextMethod("roundRect",t)}drawStar(...t){return F(O,"Graphics#drawStar has been renamed to Graphics#star"),this._callContextMethod("star",t)}}class Jl extends Vs{constructor(t,e){const{text:i,resolution:r,style:n,anchor:o,width:a,height:h,roundPixels:l,...c}=t;super({...c}),this.batched=!0,this._resolution=null,this._autoResolution=!0,this._didTextUpdate=!0,this._styleClass=e,this.text=i??"",this.style=n,this.resolution=r??null,this.allowChildren=!1,this._anchor=new q({_onUpdate:()=>{this.onViewUpdate()}}),o&&(this.anchor=o),this.roundPixels=l??!1,a!==void 0&&(this.width=a),h!==void 0&&(this.height=h)}get anchor(){return this._anchor}set anchor(t){typeof t=="number"?this._anchor.set(t):this._anchor.copyFrom(t)}set text(t){t=t.toString(),this._text!==t&&(this._text=t,this.onViewUpdate())}get text(){return this._text}set resolution(t){this._autoResolution=t===null,this._resolution=t,this.onViewUpdate()}get resolution(){return this._resolution}get style(){return this._style}set style(t){var e;t||(t={}),(e=this._style)==null||e.off("update",this.onViewUpdate,this),t instanceof this._styleClass?this._style=t:this._style=new this._styleClass(t),this._style.on("update",this.onViewUpdate,this),this.onViewUpdate()}get width(){return Math.abs(this.scale.x)*this.bounds.width}set width(t){this._setWidth(t,this.bounds.width)}get height(){return Math.abs(this.scale.y)*this.bounds.height}set height(t){this._setHeight(t,this.bounds.height)}getSize(t){return t||(t={}),t.width=Math.abs(this.scale.x)*this.bounds.width,t.height=Math.abs(this.scale.y)*this.bounds.height,t}setSize(t,e){typeof t=="object"?(e=t.height??t.width,t=t.width):e??(e=t),t!==void 0&&this._setWidth(t,this.bounds.width),e!==void 0&&this._setHeight(e,this.bounds.height)}containsPoint(t){const e=this.bounds.width,i=this.bounds.height,r=-e*this.anchor.x;let n=0;return t.x>=r&&t.x<=r+e&&(n=-i*this.anchor.y,t.y>=n&&t.y<=n+i)}onViewUpdate(){this.didViewUpdate||(this._didTextUpdate=!0),super.onViewUpdate()}destroy(t=!1){super.destroy(t),this.owner=null,this._bounds=null,this._anchor=null,(typeof t=="boolean"?t:t!=null&&t.style)&&this._style.destroy(t),this._style=null,this._text=null}get styleKey(){return`${this._text}:${this._style.styleKey}:${this._resolution}`}}function tc(s,t){let e=s[0]??{};return(typeof e=="string"||s[1])&&(F(O,`use new ${t}({ text: "hi!", style }) instead`),e={text:e,style:s[1]}),e}let At=null,gt=null;function ec(s,t){At||(At=et.get().createCanvas(256,128),gt=At.getContext("2d",{willReadFrequently:!0}),gt.globalCompositeOperation="copy",gt.globalAlpha=1),(At.width{this.pointer=null,this.lastPointerEventAt=performance.now()}),this.host.addEventListener("pointermove",t=>{const e=this.host.getBoundingClientRect();this.pointer={x:t.clientX-e.left,y:t.clientY-e.top},this.lastPointerEventAt=performance.now()})}resize(){this.bounds={width:this.host.clientWidth,height:this.host.clientHeight}}render(t,e){this.resize(),this.renderFrame(t,e),this.clearHover()}clearHover(){this.crosshair.clear()}getHoverCandidate(){if(!this.pointer||this.screenPoints.length===0)return null;let t=null,e=1/0;for(const i of this.screenPoints){const r=i.x-this.pointer.x,n=i.y-this.pointer.y,o=Math.sqrt(r*r+n*n);othis.hoverRadiusPx?null:{panelId:this.panelId,point:t,x:De(t.x,0,this.bounds.width),y:De(t.y,0,this.bounds.height),pointerX:this.pointer.x,pointerY:this.pointer.y,distance:e,lastPointerEventAt:this.lastPointerEventAt}}hasPointer(){return this.pointer!==null}findNearestScreenPointByTime(t){if(this.screenPoints.length===0)return null;let e=null,i=1/0;for(const r of this.screenPoints){const n=Math.abs(r.timeMs-t);nt;){const e=this.axisLabels.pop();this.axisLabelLayer.removeChild(e),e.destroy()}}renderAxes({padding:t,plotWidth:e,plotHeight:i,minTime:r,maxTime:n,minValue:o,maxValue:a,width:h}){const d=[];this.axes.clear(),this.axes.moveTo(t.left,t.top),this.axes.lineTo(t.left,t.top+i),this.axes.lineTo(t.left+e,t.top+i),this.axes.stroke({color:4082783,width:1,alpha:1});for(let m=0;m<5;m+=1){const g=m/4,f=t.top+g*i,x=r+g*(n-r);this.axes.moveTo(t.left-8,f),this.axes.lineTo(t.left,f),this.axes.stroke({color:5202554,width:1,alpha:1}),d.push({text:Ot(x),x:14,y:f-7,anchorX:0})}for(let m=0;m<5;m+=1){const g=m/4,f=t.left+g*e,x=o+g*(a-o);this.axes.moveTo(f,t.top+i),this.axes.lineTo(f,t.top+i+8),this.axes.stroke({color:5202554,width:1,alpha:1}),d.push({text:Ls(x),x:f,y:t.top+i+10,anchorX:.5})}this.ensureAxisLabelCount(d.length),d.forEach((m,g)=>{const f=this.axisLabels[g];f.text=m.text,f.x=m.x,f.y=m.y,f.anchor.set(m.anchorX,0)}),this.axisTitleText.text="TIME",this.axisTitleText.x=18,this.axisTitleText.y=t.top-18,this.axisTitleText.rotation=0,this.axes.moveTo(t.left+e,t.top+i),this.axes.lineTo(h-14,t.top+i),this.axes.stroke({color:2107957,width:1,alpha:1})}renderReadouts(t,e){if(!this.showReadouts){this.readoutBackground.clear(),this.realTimeText.text="",this.plotTimeText.text="";return}const i=168,r=22,n=6,o=e-i-18,a=14;this.readoutBackground.clear(),this.readoutBackground.rect(o,a,i,r),this.readoutBackground.fill({color:1054237,alpha:1}),this.readoutBackground.stroke({color:3095629,width:1,alpha:1}),this.readoutBackground.rect(o,a+r+n,i,r),this.readoutBackground.fill({color:1054237,alpha:1}),this.readoutBackground.stroke({color:3095629,width:1,alpha:1}),this.realTimeText.text=`REAL ${Un(t.time.realNowMs)}`,this.realTimeText.x=o+10,this.realTimeText.y=a+5,this.plotTimeText.text=`PLOT ${Ot(t.time.plotTimeMs)}`,this.plotTimeText.x=o+10,this.plotTimeText.y=a+r+n+5}renderFrame(t,e){const i=this.bounds.width,r=this.bounds.height,n={top:72,right:28,bottom:46,left:88},o=Math.max(10,i-n.left-n.right),a=Math.max(10,r-n.top-n.bottom),h=t.time.plotTimeMs-t.plot.windowDurationMs,l=Math.max(t.time.plotTimeMs,h+1),{min:c,max:u}=t.plot.valueRange,p=Math.max(.001,u-c);if(this.background.clear(),rc(this.background,0,0,i,r,6,{color:329483,alpha:1},{color:2898765,width:1}),this.grid.clear(),t.plot.showGrid){for(let m=0;m<=6;m+=1){const g=n.left+o*m/6;this.grid.moveTo(g,n.top),this.grid.lineTo(g,n.top+a),this.grid.stroke({color:2176074,width:1,alpha:.85})}for(let m=0;m<=8;m+=1){const g=n.top+a*m/8;this.grid.moveTo(n.left,g),this.grid.lineTo(n.left+o,g),this.grid.stroke({color:2176074,width:1,alpha:.85})}}if(this.renderAxes({padding:n,plotWidth:o,plotHeight:a,minTime:h,maxTime:l,minValue:c,maxValue:u,width:i}),this.line.clear(),this.points.clear(),this.screenPoints=[],e.length>0&&(e.forEach((d,m)=>{const g=n.left+(d.value-c)/p*o,f=n.top+(d.timeMs-h)/(l-h)*a;this.screenPoints.push({...d,x:g,y:f}),m===0?this.line.moveTo(g,f):this.line.lineTo(g,f)}),this.line.stroke({color:this.lineColor,width:2,alpha:.96,cap:"square",join:"miter"}),t.plot.showPoints))for(const d of this.screenPoints)this.points.rect(d.x-2,d.y-2,4,4),this.points.fill({color:this.pointColor,alpha:.92});this.titleText.text=this.panelTitle,this.titleText.x=20,this.titleText.y=14,this.subtitleText.text=this.panelSubtitle??`value → ${t.source.preset} · ${t.source.sampleRateHz} hz · time ↓`,this.subtitleText.x=20,this.subtitleText.y=36,this.renderReadouts(t,i)}destroy(){this.app.destroy(!0,{children:!0})}}class ti{constructor(t={}){this.config={...t},this.running=!1}start(){this.running=!0}stop(){this.running=!1}updateConfig(t){this.config={...this.config,...t}}}function nc(s,t,e){return Math.min(e,Math.max(t,s))}class oc extends ti{constructor(t={}){super({replayRate:1,dataset:[],...t}),this.sourceType="csv-replay",this.nextPointIndex=0}start(t=0){super.start(),this.reset(t)}reset(){this.nextPointIndex=0}updateConfig(t){const e=t.dataset!==this.config.dataset;super.updateConfig(t),e&&this.reset()}update(t){if(!this.running||!Array.isArray(this.config.dataset)||this.config.dataset.length===0)return[];const e=nc(this.config.replayRate??1,.1,8),i=t*e,r=[];for(;this.nextPointIndexi)break;r.push({timeMs:n.timeMs/e,value:n.value,sourceId:this.config.id??"csv-replay"}),this.nextPointIndex+=1}return r}}function ac(s,t,e){return Math.min(e,Math.max(t,s))}function hc(s){const t=Math.sin(s*12.9898)*43758.5453;return t-Math.floor(t)}class lc extends ti{constructor(t={}){super({sampleRateHz:60,preset:"telemetry",amplitude:1,noise:.08,...t}),this.sourceType="synthetic-wave",this.lastEmittedPlotTimeMs=0}start(t=0){super.start(),this.lastEmittedPlotTimeMs=t}stop(){super.stop()}reset(t=0){this.lastEmittedPlotTimeMs=t}sampleValue(t){const e=t/1e3,i=this.config.amplitude,r=this.config.noise,n=(hc(t*.017)-.5)*2*r;switch(this.config.preset){case"chirp":{const o=Math.sin(e*e*1.4);return i*(.7*o+.3*Math.sin(e*7.5))+n}case"burst":{const o=e%6-1.5,a=Math.sin(e*9.5)*Math.exp(-(o**2)*.8);return i*(.45*Math.sin(e*2.1)+a)+n}case"telemetry":default:{const o=Math.sin(e*2.2),a=.35*Math.cos(e*6.4+Math.sin(e*.8)),h=.15*Math.sin(e*.33);return i*(o+a+h)+n}}}update(t){if(!this.running)return[];const e=1e3/ac(this.config.sampleRateHz,1,240);if(tDs(t));if($n(s))return[{value:s,timestampMs:null}];if(typeof s=="string"){const t=s.trim();if(!t)return[];const e=Number(t);if(Number.isFinite(e))return[{value:e,timestampMs:null}];try{return Ds(JSON.parse(t))}catch{return[]}}if(s&&typeof s=="object"){const t=[s.value,s.y,s.signal,s.data].find(i=>Number.isFinite(Number(i)));if(t===void 0)return[];const e=[s.timeMs,s.timestampMs,s.timestamp,s.t].find(i=>Number.isFinite(Number(i)));return[{value:Number(t),timestampMs:e===void 0?null:Number(e)}]}return[]}class uc extends ti{constructor(t={},{onStatusChange:e}={}){super({wsUrl:"ws://localhost:8080",wsReconnectMs:2e3,...t}),this.sourceType="websocket",this.onStatusChange=e,this.socket=null,this.queue=[],this.lastPlotTimeMs=0,this.reconnectTimer=null,this.shouldReconnect=!1,this.firstSourceTimestampMs=null,this.basePlotTimeMs=0}start(t=0){super.start(),this.lastPlotTimeMs=t,this.basePlotTimeMs=t,this.shouldReconnect=!0,this.connect()}stop(){super.stop(),this.shouldReconnect=!1,this.clearReconnectTimer(),this.socket&&(this.socket.close(),this.socket=null),this.setStatus("disconnected","socket closed")}reset(t=0){this.queue=[],this.lastPlotTimeMs=t,this.basePlotTimeMs=t,this.firstSourceTimestampMs=null}updateConfig(t){const e=this.config.wsUrl,i=this.config.wsReconnectMs;super.updateConfig(t),(e!==this.config.wsUrl||i!==this.config.wsReconnectMs)&&this.running&&this.reconnect()}update(t){if(this.lastPlotTimeMs=t,this.queue.length===0)return[];const e=[];for(;this.queue.length>0;){const i=this.queue.shift();let r=t;$n(i.timestampMs)&&(this.firstSourceTimestampMs===null&&(this.firstSourceTimestampMs=i.timestampMs,this.basePlotTimeMs=t),r=this.basePlotTimeMs+(i.timestampMs-this.firstSourceTimestampMs)),e.push({timeMs:r,value:i.value,sourceId:this.config.id??"websocket"})}return e}reconnect(){this.running&&(this.clearReconnectTimer(),this.socket&&(this.socket.close(),this.socket=null),this.connect())}connect(){var e;const t=(e=this.config.wsUrl)==null?void 0:e.trim();if(!t){this.setStatus("idle","enter a websocket url");return}this.clearReconnectTimer(),this.setStatus("connecting",t);try{this.socket=new WebSocket(t)}catch(i){this.setStatus("error",i instanceof Error?i.message:String(i)),this.scheduleReconnect();return}this.socket.addEventListener("open",()=>{this.setStatus("connected",t)}),this.socket.addEventListener("message",i=>{const r=Ds(i.data);r.length!==0&&this.queue.push(...r)}),this.socket.addEventListener("error",()=>{this.setStatus("error","socket error")}),this.socket.addEventListener("close",()=>{this.socket=null,this.running&&(this.setStatus("disconnected","retrying"),this.scheduleReconnect())})}scheduleReconnect(){if(!this.shouldReconnect||!this.running)return;const t=cc(Number(this.config.wsReconnectMs)||2e3,250,3e4);this.clearReconnectTimer(),this.reconnectTimer=window.setTimeout(()=>{this.connect()},t)}clearReconnectTimer(){this.reconnectTimer!==null&&(window.clearTimeout(this.reconnectTimer),this.reconnectTimer=null)}setStatus(t,e=""){var i;(i=this.onStatusChange)==null||i.call(this,{wsStatus:t,wsStatusDetail:e})}}class dc{constructor(t,e){this.store=t,this.bus=e,this.sources=new Map,this.syncFromState()}syncFromState(){const t=this.store.getState(),e=Object.entries(t.sources),i=new Set(e.map(([r])=>r));for(const[r,n]of e){const o=this.sources.get(r);if(!o){const a=this.createSource(r,n);this.sources.set(r,a),a.start(t.time.plotTimeMs);continue}if(o.sourceType!==n.type){o.stop();const a=this.createSource(r,n);this.sources.set(r,a),a.start(t.time.plotTimeMs);continue}o.updateConfig(n)}for(const[r,n]of this.sources.entries())i.has(r)||(n.stop(),this.sources.delete(r))}createSource(t,e){switch(e.type){case"csv-replay":return new oc(e);case"websocket":return new uc(e,{onStatusChange:i=>{this.store.setState(r=>({...r,sources:{...r.sources,[t]:{...r.sources[t],...i}}}))}});case"synthetic-wave":default:return new lc(e)}}update(t){for(const[e,i]of this.sources.entries()){const r=i.update(t);for(const n of r)this.bus.emit("data:point",{...n,sourceId:e})}}reset(){const t=this.store.getState().time.plotTimeMs;for(const e of this.sources.values())e.reset(t)}}function fc(s){return s.split(/[;,\t]/).map(t=>t.trim())}function zs(s){return s!==""&&Number.isFinite(Number(s))}function pc(s){if(s.length===0)return{hasHeader:!1,headers:[]};const[t]=s,e=t.some(i=>!zs(i));return{hasHeader:e,headers:e?t.map(i=>i.toLowerCase()):[]}}function mc(s){const t=s.find(e=>e.includes("time")||e.includes("timestamp"));return t&&t.includes("sec")&&!t.includes("msec")&&!t.includes("ms")?1e3:1}function gc(s,t){if(s.length===0)return{timeIndex:t>1?0:-1,valueIndex:t>1?1:0};const e=s.findIndex(r=>r.includes("time")||r.includes("timestamp")),i=s.findIndex(r=>r.includes("value")||r.includes("signal")||r.includes("y"));return{timeIndex:e,valueIndex:i>=0?i:s.length>1?1:0}}function xc(s,{sampleRateHz:t=60}={}){var m;const e=s.split(/\r?\n/).map(g=>g.trim()).filter(g=>g&&!g.startsWith("#")).map(fc).filter(g=>g.some(f=>f!==""));if(e.length===0)throw new Error("CSV file is empty");const{hasHeader:i,headers:r}=pc(e),n=i?e.slice(1):e,o=e[0].length,{timeIndex:a,valueIndex:h}=gc(r,o),l=mc(r),c=1e3/Math.max(1,t),u=n.map((g,f)=>{const x=g[h];if(!zs(x))return null;const y=Number(x);return{timeMs:a>=0&&zs(g[a])?Number(g[a])*l:f*c,value:y}}).filter(Boolean).sort((g,f)=>g.timeMs-f.timeMs);if(u.length===0)throw new Error("CSV file did not contain any numeric data points");const p=u[0].timeMs,d=u.map(g=>({timeMs:g.timeMs-p,value:g.value}));return{points:d,metadata:{pointCount:d.length,durationMs:((m=d.at(-1))==null?void 0:m.timeMs)??0}}}function H(s,t,e){const i=document.createElement(s);return t&&(i.className=t),e&&(i.textContent=e),i}function mr(s,t){s.dataset.active=String(t)}function gr(s){return s.tagName==="SELECT"?s.value:s instanceof HTMLInputElement?s.type==="checkbox"?s.checked:s.type==="number"||s.type==="range"?Number(s.value):s.value:s.value}function j(s,t){if(!(!s||document.activeElement===s)){if(s instanceof HTMLInputElement&&s.type==="checkbox"){s.checked=!!t;return}s.value=String(t??"")}}class yc{constructor({root:t,store:e,actions:i}){this.root=t,this.store=e,this.actions=i,this.elements={}}mount(){const t=H("div","timeplot-shell"),e=H("header","timeplot-topbar"),i=H("section","timeplot-viewport"),r=H("div","timeplot-plot-grid"),n=H("section","timeplot-plot-panel"),o=H("section","timeplot-plot-panel"),a=H("div","timeplot-canvas-host"),h=H("div","timeplot-canvas-host"),l=H("aside","timeplot-sidebar"),c=H("div","timeplot-tooltip"),u=H("div","timeplot-tooltip");c.hidden=!0,u.hidden=!0;const p=H("div","timeplot-brand"),d=H("h1","timeplot-title","TimePlot"),m=H("div","timeplot-subtitle","Dual synchronized signal monitor");p.append(d,m);const g=H("div","timeplot-toolbar");return g.append(this.createTransportControls(),this.createPanelToggles()),e.append(p,g),n.append(a,c),o.append(h,u),r.append(n,o),i.append(r),t.append(e,i,l),this.root.replaceChildren(t),this.elements={...this.elements,shell:t,topbar:e,viewport:i,plotGrid:r,primaryPlotPanel:n,secondaryPlotPanel:o,primaryCanvasHost:a,secondaryCanvasHost:h,sidebar:l,primaryTooltip:c,secondaryTooltip:u,title:d,subtitle:m,statusPanel:this.createStatusPanel(),sourcePanel:this.createSourcePanel(),configPanel:this.createConfigPanel(),helpPanel:this.createHelpPanel()},l.append(this.elements.statusPanel,this.elements.sourcePanel,this.elements.configPanel,this.elements.helpPanel),this.elements}createTransportControls(){const t=H("div","control-group"),e=H("button","control-button","Pause"),i=H("button","control-button","Reset"),r=H("span",null,"Speed"),n=document.createElement("input");n.type="range",n.min="0.1",n.max="6",n.step="0.1";const o=H("span",null,"1.0×");return e.addEventListener("click",()=>this.actions.togglePause()),i.addEventListener("click",()=>this.actions.resetScene()),n.addEventListener("input",a=>this.actions.setSpeed(Number(a.target.value))),t.append(e,i,r,n,o),this.elements.pauseButton=e,this.elements.resetButton=i,this.elements.speedInput=n,this.elements.speedValue=o,t}createPanelToggles(){const t=H("div","control-group"),e=["status","source","config","help"];this.elements.panelButtons={};for(const i of e){const r=H("button","panel-toggle",i);r.addEventListener("click",()=>this.actions.togglePanel(i)),this.elements.panelButtons[i]=r,t.append(r)}return t}createStatusPanel(){const t=H("section","panel");return t.innerHTML=` +

Status

+
+
Renderer
+
Real time
+
Real elapsed
+
Plot time
+
Playback
+
Points
+
+ `,t}createSourcePanel(){const t=H("section","panel");return t.innerHTML=` +

Data Source

+
+
Signal A
+
+ +
+
+ + + + +
+
+ + +
+
+
+ + +
+
+
+
+
Signal B
+
+ +
+
+ + + + +
+
+ + +
+
+
+ + +
+
+
+ `,t.querySelectorAll("[data-source-field]").forEach(e=>{const i=e.tagName==="SELECT"?"change":"input";e.addEventListener(i,()=>{const r=e.getAttribute("data-source-key"),n=e.getAttribute("data-source-field"),o=gr(e);this.actions.updateSource(r,n,o)})}),t.querySelectorAll("[data-source-file]").forEach(e=>{e.addEventListener("change",async()=>{var n;const i=e.getAttribute("data-source-key"),r=(n=e.files)==null?void 0:n[0];r&&(await this.actions.loadSourceFile(i,r),e.value="")})}),t}createConfigPanel(){const t=H("section","panel");return t.innerHTML=` +

Config

+
+ + +
+ Show grid + +
+
+ Show points + +
+
+
+
Graph routing
+
+ + + + +
+
+ `,t.querySelectorAll("[data-plot-field]").forEach(e=>{const i=e instanceof HTMLInputElement&&e.type==="checkbox"?"change":"input";e.addEventListener(i,()=>{const r=e.getAttribute("data-plot-field"),n=gr(e);this.actions.updatePlot(r,n)})}),t.querySelectorAll("[data-graph-field]").forEach(e=>{e.addEventListener("change",()=>{const i=e.getAttribute("data-graph-id"),r=e.getAttribute("data-graph-field");this.actions.updateGraph(i,r,e.value)})}),t}createHelpPanel(){const t=H("section","panel");return t.innerHTML=` +

Help

+
    +
  1. Each signal can be synthetic or file-backed CSV replay.
  2. +
  3. Each graph can target Signal A or Signal B independently.
  4. +
  5. Each graph can render raw, delta, or smoothed data.
  6. +
  7. Hover either trace to inspect the nearest synchronized sample.
  8. +
  9. Use pause and speed controls to inspect timing behavior.
  10. +
+ `,t}sync(t,e){this.elements.title.textContent=t.app.title,this.elements.subtitle.textContent="Dual synchronized signal monitor",this.elements.pauseButton.textContent=t.time.paused?"Resume":"Pause",mr(this.elements.pauseButton,t.time.paused),j(this.elements.speedInput,t.time.speed),this.elements.speedValue.textContent=`${t.time.speed.toFixed(1)}×`;const i=this.elements.statusPanel.querySelectorAll("[data-field]"),r=Object.fromEntries(Array.from(i).map(n=>[n.getAttribute("data-field"),n]));r.renderer.textContent=t.app.renderer,r.realTime.textContent=Un(t.time.realNowMs),r.realElapsed.textContent=Ot(t.time.realElapsedMs),r.plotTime.textContent=Ot(t.time.plotTimeMs),r.playback.textContent=t.time.paused?"Paused":`${t.time.speed.toFixed(1)}×`,r.points.textContent=typeof e=="object"?`${e.primary} / ${e.secondary}`:`${e}`,this.syncSourcePanel(t),this.syncConfigPanel(t),this.syncPanels(t),this.syncTooltip(t)}syncSourcePanel(t){Object.entries(t.sources).forEach(([e,i])=>{j(this.elements.sourcePanel.querySelector(`[data-source-key="${e}"][data-source-field="type"]`),i.type),j(this.elements.sourcePanel.querySelector(`[data-source-key="${e}"][data-source-field="preset"]`),i.preset),j(this.elements.sourcePanel.querySelector(`[data-source-key="${e}"][data-source-field="sampleRateHz"]`),i.sampleRateHz),j(this.elements.sourcePanel.querySelector(`[data-source-key="${e}"][data-source-field="amplitude"]`),i.amplitude),j(this.elements.sourcePanel.querySelector(`[data-source-key="${e}"][data-source-field="noise"]`),i.noise);const r=this.elements.sourcePanel.querySelector(`[data-source-key="${e}"][data-source-field="replayRate"]`);r&&j(r,i.replayRate??1),this.elements.sourcePanel.querySelector(`[data-source-config="${e}"]`).querySelectorAll("[data-source-mode]").forEach(c=>{c.hidden=c.getAttribute("data-source-mode")!==i.type});const o=this.elements.sourcePanel.querySelector(`[data-source-key="${e}"][data-source-meta]`);o&&(i.type==="csv-replay"?o.innerHTML=i.loadError?`${i.loadError}`:`${i.dataFileName||"No file loaded"}${i.datasetPointCount?` · ${i.datasetPointCount} pts · ${Ot(i.datasetDurationMs||0)}`:""}`:i.type==="websocket"?o.textContent="":o.textContent="Generates data procedurally in-browser");const a=this.elements.sourcePanel.querySelector(`[data-source-key="${e}"][data-source-field="wsUrl"]`),h=this.elements.sourcePanel.querySelector(`[data-source-key="${e}"][data-source-field="wsReconnectMs"]`),l=this.elements.sourcePanel.querySelector(`[data-source-key="${e}"][data-source-ws-meta]`);a&&j(a,i.wsUrl??""),h&&j(h,i.wsReconnectMs??2e3),l&&(l.innerHTML=i.type==="websocket"?`status: ${i.wsStatus||"idle"}${i.wsStatusDetail?` · ${i.wsStatusDetail}`:""}`:"")})}syncConfigPanel(t){j(this.elements.configPanel.querySelector('[data-plot-field="windowDurationMs"]'),t.plot.windowDurationMs),j(this.elements.configPanel.querySelector('[data-plot-field="maxPoints"]'),t.plot.maxPoints),j(this.elements.configPanel.querySelector('[data-plot-field="showGrid"]'),t.plot.showGrid),j(this.elements.configPanel.querySelector('[data-plot-field="showPoints"]'),t.plot.showPoints),j(this.elements.configPanel.querySelector('[data-graph-id="primary"][data-graph-field="sourceKey"]'),t.graphs.primary.sourceKey),j(this.elements.configPanel.querySelector('[data-graph-id="primary"][data-graph-field="transform"]'),t.graphs.primary.transform),j(this.elements.configPanel.querySelector('[data-graph-id="secondary"][data-graph-field="sourceKey"]'),t.graphs.secondary.sourceKey),j(this.elements.configPanel.querySelector('[data-graph-id="secondary"][data-graph-field="transform"]'),t.graphs.secondary.transform)}syncPanels(t){const e={status:this.elements.statusPanel,source:this.elements.sourcePanel,config:this.elements.configPanel,help:this.elements.helpPanel};for(const[i,r]of Object.entries(t.panels))e[i].hidden=!r.visible,mr(this.elements.panelButtons[i],r.visible)}syncTooltip(t){const e=t.plot.tooltip;if(this.elements.primaryTooltip.hidden=!0,this.elements.secondaryTooltip.hidden=!0,!e.visible||!e.point)return;const i=e.panelId==="secondary"?this.elements.secondaryTooltip:this.elements.primaryTooltip;i.hidden=!1,i.style.left=`${e.x}px`,i.style.top=`${e.y}px`,i.innerHTML=` +
Hovered sample
+
Panel${e.panelLabel??"Primary"}
+
Plot time${Ot(e.point.timeMs)}
+
Value${Ls(e.point.value)}
+
Source${e.point.sourceId}
+ ${e.linkedPoint?`
Linked panel${e.linkedPanelLabel??"Linked"}
`:""} + ${e.linkedPoint?`
Linked value${Ls(e.linkedPoint.value)}
`:""} + `}}function xr(s,t,e){return Math.min(e,Math.max(t,s))}function _c(s){if(s.length<2)return[];const t=[];for(let e=1;ea+h.value,0)/n.length;e.push({...s[i],value:o,sourceId:`${s[i].sourceId}:smooth`})}return e}function wc(s,t){switch(t){case"delta":return _c(s);case"smooth":return bc(s);case"raw":default:return s}}function yr(s){switch(s){case"delta":return"Δvalue / second";case"smooth":return"moving average";case"raw":default:return"raw signal"}}function Sc(s,t){if(s.length===0)return t;let e=1/0,i=-1/0;for(const n of s)e=Math.min(e,n.value),i=Math.max(i,n.value);const r=Math.max(Math.abs(e),Math.abs(i),.1);return{min:-r,max:r}}function vc(s,t){return!s&&!t?null:s&&!t?s:!s&&t?t:s.lastPointerEventAt>=t.lastPointerEventAt?s:t}async function Pc(s){const t=new Wn,e=new Kn(br()),i=new Qn(e),r=new Map(Object.keys(e.getState().sources).map(f=>[f,new ni(e.getState().plot.maxPoints)]));let n;const o=()=>{const f=e.getState();for(const x of Object.keys(f.sources))r.has(x)||r.set(x,new ni(f.plot.maxPoints)),r.get(x).maxPoints=f.plot.maxPoints;for(const x of Array.from(r.keys()))f.sources[x]||r.delete(x)},a=f=>{var x;(x=r.get(f))==null||x.clear()},h=(f,x)=>{const y=f.graphs[x],_=r.get(y.sourceKey),b=_?_.getVisiblePoints(f.time.plotTimeMs,f.plot.windowDurationMs):[],v=wc(b,y.transform);return{graphConfig:y,points:v,range:Sc(v,f.plot.valueRange)}},l={togglePause:()=>i.togglePause(),setSpeed:f=>i.setSpeed(f),resetScene:()=>{i.reset(),r.forEach(f=>f.clear()),n.reset()},togglePanel:f=>{e.setState(x=>({...x,panels:{...x.panels,[f]:{...x.panels[f],visible:!x.panels[f].visible}}}))},updateSource:(f,x,y)=>{e.setState(_=>({..._,sources:{..._.sources,[f]:{..._.sources[f],[x]:y,...x==="type"?{loadError:y==="csv-replay"&&_.sources[f].dataset.length===0?_.sources[f].dataFileName?`Reload ${_.sources[f].dataFileName} to restore replay data`:"Load a CSV file to begin replay":"",wsStatus:y==="websocket"?_.sources[f].wsStatus:"idle",wsStatusDetail:y==="websocket"?_.sources[f].wsStatusDetail:""}:{}}}})),n.syncFromState(),o(),(x==="type"||x==="wsUrl"||x==="wsReconnectMs")&&(a(f),n.reset())},loadSourceFile:async(f,x)=>{var y;try{const b=((y=e.getState().sources[f])==null?void 0:y.sampleRateHz)??60,v=await x.text(),{points:S,metadata:w}=xc(v,{sampleRateHz:b});a(f),e.setState(R=>({...R,sources:{...R.sources,[f]:{...R.sources[f],type:"csv-replay",dataset:S,dataFileName:x.name,datasetPointCount:w.pointCount,datasetDurationMs:w.durationMs,loadError:"",wsStatus:"idle",wsStatusDetail:""}}})),n.syncFromState(),n.reset()}catch(_){e.setState(b=>({...b,sources:{...b.sources,[f]:{...b.sources[f],loadError:_ instanceof Error?_.message:String(_)}}}))}},updatePlot:(f,x)=>{e.setState(y=>({...y,plot:{...y.plot,[f]:x}})),f==="maxPoints"&&(buffer.maxPoints=xr(x,200,4e3),r.forEach(y=>{y.maxPoints=xr(x,200,4e3)}))},updateGraph:(f,x,y)=>{e.setState(_=>({..._,graphs:{..._.graphs,[f]:{..._.graphs[f],[x]:y}}}))}},c=new yc({root:s,store:e,actions:l}),u=c.mount(),p=new pr({host:u.primaryCanvasHost,panelId:"primary",title:"Primary signal",subtitle:null,showReadouts:!0,lineColor:10473983,pointColor:15201023}),d=new pr({host:u.secondaryCanvasHost,panelId:"secondary",title:"Secondary signal",subtitle:null,showReadouts:!1,lineColor:16761963,pointColor:16769456}),m=await p.init();await d.init(),e.patch({app:{...e.getState().app,renderer:m}}),n=new dc(e,t),t.on("data:point",f=>{var x;(x=r.get(f.sourceId))==null||x.addPoint(f)});const g=f=>{if(!(f.target instanceof HTMLInputElement||f.target instanceof HTMLSelectElement)){if(f.code==="Space"){f.preventDefault(),l.togglePause();return}if(f.key==="["){l.setSpeed(e.getState().time.speed-.1);return}if(f.key==="]"){l.setSpeed(e.getState().time.speed+.1);return}f.key.toLowerCase()==="g"&&l.updatePlot("showGrid",!e.getState().plot.showGrid)}};return window.addEventListener("keydown",g),p.app.ticker.add(()=>{i.tick(),n.syncFromState(),o(),n.update(e.getState().time.plotTimeMs);const f=e.getState(),x=h(f,"primary"),y=h(f,"secondary");p.panelTitle=f.graphs.primary.title,p.panelSubtitle=`${f.sources[f.graphs.primary.sourceKey].label} · ${yr(f.graphs.primary.transform)} · time ↓`,d.panelTitle=f.graphs.secondary.title,d.panelSubtitle=`${f.sources[f.graphs.secondary.sourceKey].label} · ${yr(f.graphs.secondary.transform)} · time ↓`;const _={...f,plot:{...f.plot,valueRange:x.range}},b={...f,plot:{...f.plot,valueRange:y.range}};p.render(_,x.points),d.render(b,y.points);const v=p.getHoverCandidate(),S=d.getHoverCandidate(),w=vc(v,S);if(!w){p.clearHover(),d.clearHover(),e.setState(E=>({...E,plot:{...E.plot,hoveredPoint:null,tooltip:{...E.plot.tooltip,visible:!1,point:null,linkedPoint:null}}})),c.sync(e.getState(),{primary:x.points.length,secondary:y.points.length});return}const R=p.findNearestScreenPointByTime(w.point.timeMs),k=d.findNearestScreenPointByTime(w.point.timeMs);p.renderLinkedHover(R),d.renderLinkedHover(k);const P=w.panelId==="secondary"?f.graphs.secondary.title:f.graphs.primary.title,M=w.panelId==="secondary"?R:k,G=w.panelId==="secondary"?f.graphs.primary.title:f.graphs.secondary.title;e.setState(E=>({...E,plot:{...E.plot,hoveredPoint:w.point,tooltip:{...E.plot.tooltip,visible:!0,panelId:w.panelId,panelLabel:P,x:w.x,y:w.y,point:w.point,linkedPoint:M,linkedPanelLabel:G}}})),c.sync(e.getState(),{primary:x.points.length,secondary:y.points.length})}),{destroy(){window.removeEventListener("keydown",g),p.destroy(),d.destroy()}}}const Hs=document.getElementById("app");if(!Hs)throw new Error("App root not found");Pc(Hs).catch(s=>{console.error("Failed to start TimePlot",s),Hs.innerHTML=` +
+

TimePlot failed to start

+
${String(s)}
+
+ `});export{Wo as $,xn as A,J as B,ft as C,et as D,L as E,Nr as F,Ye as G,bi as H,de as I,ga as J,Ma as K,_e as L,I as M,V as N,W as O,Z as P,Oa as Q,Cs as R,dh as S,ke as T,vs as U,Si as V,rs as W,vi as X,Xo as Y,Ue as Z,Y as _,pt as a,Lr as a0,yn as a1,F as a2,O as a3,ja as a4,Sh as a5,mh as a6,Uh as a7,Oh as a8,jh as a9,Zh as aa,Kh as ab,Sn as ac,Nt as ad,$e as ae,lr as af,Nn as ag,Za as ah,Ht as ai,In as aj,Hi as ak,Di as al,dt as am,po as an,fr as ao,Gn as ap,xe as b,He as c,Ws as d,rt as e,zi as f,lh as g,pn as h,Xr as i,ht as j,ln as k,Gs as l,Nh as m,$h as n,Yh as o,qh as p,Ys as q,Ao as r,D as s,nn as t,fo as u,Jh as v,Q as w,it as x,Br as y,at as z}; diff --git a/dist/assets/index-DCiDMyds.css b/dist/assets/index-DCiDMyds.css new file mode 100644 index 0000000..5b91335 --- /dev/null +++ b/dist/assets/index-DCiDMyds.css @@ -0,0 +1 @@ +:root{color-scheme:dark;--bg: #0a0c10;--bg-alt: #0f1319;--surface: #11161d;--surface-strong: #0d1117;--surface-raised: #171d26;--border: #28313d;--border-strong: #394657;--text: #edf2f7;--muted: #97a3b4;--accent: #9fc7ff;--accent-strong: #d8e8ff;--shadow: none;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}*{box-sizing:border-box}html,body,#app{width:100%;height:100%;margin:0}body{background:linear-gradient(180deg,#080a0d,#0d1015);color:var(--text);overflow:hidden}button,input,select{font:inherit}.timeplot-shell{display:grid;grid-template-columns:minmax(0,1fr) 340px;grid-template-rows:auto minmax(0,1fr);width:100%;height:100%;gap:10px;padding:10px}.timeplot-topbar{grid-column:1 / -1;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:12px 14px;border:1px solid var(--border-strong);background:var(--surface);border-radius:4px;box-shadow:var(--shadow)}.timeplot-brand{display:flex;flex-direction:column;gap:2px}.timeplot-title{margin:0;font-size:1rem;letter-spacing:.08em;text-transform:uppercase;font-weight:700}.timeplot-subtitle{color:var(--muted);font-size:.78rem;letter-spacing:.04em;text-transform:uppercase}.timeplot-toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end}.control-group{display:flex;align-items:center;gap:8px;padding:6px 8px;background:var(--surface-raised);border:1px solid var(--border);border-radius:3px}.control-group label,.control-group span{color:var(--muted);font-size:.74rem;letter-spacing:.08em;text-transform:uppercase}.control-group input[type=range]{width:118px}.control-group input[type=range]{accent-color:var(--accent)}.control-button,.panel-toggle{color:var(--text);background:var(--surface);border:1px solid var(--border-strong);border-radius:2px;padding:7px 11px;cursor:pointer;transition:border-color .12s ease,background .12s ease,color .12s ease;text-transform:uppercase;letter-spacing:.08em;font-size:.72rem;line-height:1}.control-button:hover,.panel-toggle:hover{border-color:var(--accent);color:var(--accent-strong)}.control-button[data-active=true],.panel-toggle[data-active=true]{background:#1a2230;border-color:var(--accent);color:var(--accent-strong)}.timeplot-viewport{position:relative;min-height:0;border-radius:4px;overflow:hidden;border:1px solid var(--border-strong);background:#06080b;box-shadow:var(--shadow);padding:10px}.timeplot-plot-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px;width:100%;height:100%;min-height:0}.timeplot-plot-panel{position:relative;min-width:0;min-height:0;border:1px solid var(--border);background:#070a0d}.timeplot-canvas-host{width:100%;height:100%}.timeplot-sidebar{display:flex;flex-direction:column;gap:10px;min-height:0;overflow-y:auto;padding-right:2px}.panel{border:1px solid var(--border-strong);background:var(--surface-strong);border-radius:4px;padding:14px}.panel[hidden]{display:none}.panel h2{margin:0 0 12px;font-size:.8rem;letter-spacing:.08em;text-transform:uppercase}.panel-subsection+.panel-subsection{margin-top:14px;padding-top:14px;border-top:1px solid var(--border)}.panel-section-title{margin-bottom:10px;color:var(--accent-strong);font-size:.72rem;letter-spacing:.08em;text-transform:uppercase}.kv-list{display:grid;grid-template-columns:auto 1fr;gap:10px 12px;align-items:center;margin:0}.kv-list dt{color:var(--muted);font-size:.73rem;letter-spacing:.05em;text-transform:uppercase}.kv-list dd{margin:0;text-align:right;font-variant-numeric:tabular-nums;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace}.field-grid{display:grid;gap:12px}.field-grid label{display:grid;gap:6px;color:var(--muted);font-size:.74rem;letter-spacing:.05em;text-transform:uppercase}.field-grid[data-source-mode][hidden]{display:none}.source-meta{min-height:20px;color:var(--muted);font-size:.76rem;line-height:1.4}.source-meta-error{color:#ff9d9d}.source-meta-status{text-transform:uppercase;letter-spacing:.06em}.source-meta-status-connected{color:#99e2b4}.source-meta-status-connecting{color:#ffd27f}.source-meta-status-disconnected,.source-meta-status-idle{color:var(--muted)}.source-meta-status-error{color:#ff9d9d}.field-grid input,.field-grid select{width:100%;padding:9px 10px;border-radius:2px;border:1px solid var(--border);background:var(--surface-raised);color:var(--text)}.field-grid input:focus,.field-grid select:focus{outline:none;border-color:var(--accent)}.panel-row{display:flex;align-items:center;justify-content:space-between;gap:10px;color:var(--muted);font-size:.74rem;letter-spacing:.05em;text-transform:uppercase}.panel-row+.panel-row{margin-top:10px}.panel-row input[type=checkbox]{inline-size:16px;block-size:16px;accent-color:var(--accent)}.muted{color:var(--muted)}.help-list{display:grid;gap:8px;margin:0;padding-left:18px;color:var(--muted);font-size:.82rem}.timeplot-tooltip{position:absolute;min-width:180px;padding:10px 12px;border-radius:3px;border:1px solid var(--border-strong);background:#0d1218;box-shadow:var(--shadow);pointer-events:none;transform:translate(12px,-50%);z-index:10}.timeplot-tooltip[hidden]{display:none}.timeplot-tooltip-title{margin-bottom:8px;font-size:.72rem;color:var(--accent-strong);letter-spacing:.08em;text-transform:uppercase}.timeplot-tooltip-row{display:flex;justify-content:space-between;gap:16px;font-size:.78rem}.timeplot-tooltip-row+.timeplot-tooltip-row{margin-top:4px}.timeplot-empty{color:var(--muted);font-size:.85rem}@media (max-width: 1100px){.timeplot-shell{grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(360px,1fr) auto}.timeplot-plot-grid{grid-template-columns:minmax(0,1fr);grid-template-rows:repeat(2,minmax(260px,1fr))}.timeplot-sidebar{overflow:visible}} diff --git a/dist/assets/webworkerAll-J8ccMaq4.js b/dist/assets/webworkerAll-J8ccMaq4.js new file mode 100644 index 0000000..1626284 --- /dev/null +++ b/dist/assets/webworkerAll-J8ccMaq4.js @@ -0,0 +1,296 @@ +import{E as g,U as ot,T as de,a as ut,a2 as w,a3 as U,s as C,ac as he,Z as W,ad as D,O as lt,l as Y,ae as ct,af as fe,_ as Ce,ag as I,ah as dt,ai as P,w as X,M as R,k as z,c as Se,F,a5 as se,R as ie,z as Pe,b as O,B,D as ne,x as K,aj as ht,ak as ae,J as te,al as L,q as oe,t as ft,G as pt,m as Fe,p as Be,a7 as Re,aa as Me,n as gt,o as mt,a8 as xt,a9 as _t,ab as yt,am as bt,an as vt,ao as N,e as T,ap as Tt}from"./index-DB6B0XuI.js";import{S as j,c as $,a as wt,b as Ct,B as Ue}from"./colorToUniform-DmtBy-2V.js";class Ge{static init(e){Object.defineProperty(this,"resizeTo",{set(t){globalThis.removeEventListener("resize",this.queueResize),this._resizeTo=t,t&&(globalThis.addEventListener("resize",this.queueResize),this.resize())},get(){return this._resizeTo}}),this.queueResize=()=>{this._resizeTo&&(this._cancelResize(),this._resizeId=requestAnimationFrame(()=>this.resize()))},this._cancelResize=()=>{this._resizeId&&(cancelAnimationFrame(this._resizeId),this._resizeId=null)},this.resize=()=>{if(!this._resizeTo)return;this._cancelResize();let t,r;if(this._resizeTo===globalThis.window)t=globalThis.innerWidth,r=globalThis.innerHeight;else{const{clientWidth:s,clientHeight:i}=this._resizeTo;t=s,r=i}this.renderer.resize(t,r),this.render()},this._resizeId=null,this._resizeTo=null,this.resizeTo=e.resizeTo||null}static destroy(){globalThis.removeEventListener("resize",this.queueResize),this._cancelResize(),this._cancelResize=null,this.queueResize=null,this.resizeTo=null,this.resize=null}}Ge.extension=g.Application;class Ae{static init(e){e=Object.assign({autoStart:!0,sharedTicker:!1},e),Object.defineProperty(this,"ticker",{set(t){this._ticker&&this._ticker.remove(this.render,this),this._ticker=t,t&&t.add(this.render,this,ot.LOW)},get(){return this._ticker}}),this.stop=()=>{this._ticker.stop()},this.start=()=>{this._ticker.start()},this._ticker=null,this.ticker=e.sharedTicker?de.shared:new de,e.autoStart&&this.start()}static destroy(){if(this._ticker){const e=this._ticker;this.ticker=null,e.destroy()}}}Ae.extension=g.Application;class St extends ut{constructor(){super(...arguments),this.chars=Object.create(null),this.lineHeight=0,this.fontFamily="",this.fontMetrics={fontSize:0,ascent:0,descent:0},this.baseLineOffset=0,this.distanceField={type:"none",range:0},this.pages=[],this.applyFillAsTint=!0,this.baseMeasurementFontSize=100,this.baseRenderedFontSize=100}get font(){return w(U,"BitmapFont.font is deprecated, please use BitmapFont.fontFamily instead."),this.fontFamily}get pageTextures(){return w(U,"BitmapFont.pageTextures is deprecated, please use BitmapFont.pages instead."),this.pages}get size(){return w(U,"BitmapFont.size is deprecated, please use BitmapFont.fontMetrics.fontSize instead."),this.fontMetrics.fontSize}get distanceFieldRange(){return w(U,"BitmapFont.distanceFieldRange is deprecated, please use BitmapFont.distanceField.range instead."),this.distanceField.range}get distanceFieldType(){return w(U,"BitmapFont.distanceFieldType is deprecated, please use BitmapFont.distanceField.type instead."),this.distanceField.type}destroy(e=!1){var t;this.emit("destroy",this),this.removeAllListeners();for(const r in this.chars)(t=this.chars[r].texture)==null||t.destroy();this.chars=null,e&&(this.pages.forEach(r=>r.texture.destroy(!0)),this.pages=null)}}const ze=class ke extends St{constructor(e){super(),this.resolution=1,this.pages=[],this._padding=0,this._measureCache=Object.create(null),this._currentChars=[],this._currentX=0,this._currentY=0,this._currentMaxCharHeight=0,this._currentPageIndex=-1,this._skipKerning=!1;const t={...ke.defaultOptions,...e};this._textureSize=t.textureSize,this._mipmap=t.mipmap;const r=t.style.clone();t.overrideFill&&(r._fill.color=16777215,r._fill.alpha=1,r._fill.texture=C.WHITE,r._fill.fill=null),this.applyFillAsTint=t.overrideFill;const s=r.fontSize;r.fontSize=this.baseMeasurementFontSize;const i=he(r);t.overrideSize?r._stroke&&(r._stroke.width*=this.baseRenderedFontSize/s):r.fontSize=this.baseRenderedFontSize=s,this._style=r,this._skipKerning=t.skipKerning??!1,this.resolution=t.resolution??1,this._padding=t.padding??4,t.textureStyle&&(this._textureStyle=t.textureStyle instanceof W?t.textureStyle:new W(t.textureStyle)),this.fontMetrics=D.measureFont(i),this.lineHeight=r.lineHeight||this.fontMetrics.fontSize||r.fontSize}ensureCharacters(e){var x,p;const t=D.graphemeSegmenter(e).filter(_=>!this._currentChars.includes(_)).filter((_,b,v)=>v.indexOf(_)===b);if(!t.length)return;this._currentChars=[...this._currentChars,...t];let r;this._currentPageIndex===-1?r=this._nextPage():r=this.pages[this._currentPageIndex];let{canvas:s,context:i}=r.canvasAndContext,n=r.texture.source;const a=this._style;let u=this._currentX,l=this._currentY,d=this._currentMaxCharHeight;const c=this.baseRenderedFontSize/this.baseMeasurementFontSize,h=this._padding*c;let f=!1;const y=s.width/this.resolution,m=s.height/this.resolution;for(let _=0;_y&&(l+=d,d=E,u=0,l+d>m)){n.update();const A=this._nextPage();s=A.canvasAndContext.canvas,i=A.canvasAndContext.context,n=A.texture.source,u=0,l=0,d=0}const nt=S/c-(((x=a.dropShadow)==null?void 0:x.distance)??0)-(((p=a._stroke)==null?void 0:p.width)??0);if(this.chars[b]={id:b.codePointAt(0),xOffset:-this._padding,yOffset:-this._padding,xAdvance:nt,kerning:{}},f){this._drawGlyph(i,v,u+h,l+h,c,a);const A=n.width*c,ce=n.height*c,at=new lt(u/A*n.width,l/ce*n.height,M/A*n.width,E/ce*n.height);this.chars[b].texture=new C({source:n,frame:at}),u+=Math.ceil(M)}}n.update(),this._currentX=u,this._currentY=l,this._currentMaxCharHeight=d,this._skipKerning&&this._applyKerning(t,i)}get pageTextures(){return w(U,"BitmapFont.pageTextures is deprecated, please use BitmapFont.pages instead."),this.pages}_applyKerning(e,t){const r=this._measureCache;for(let s=0;s{const _=i.width;for(let b=0;b{let p=i.chars.length-1;if(r){let _=i.chars[p];for(;_===" ";)i.width-=t.chars[_].xAdvance,_=i.chars[--p]}s.width=Math.max(s.width,i.width),i={width:0,charPositions:[],chars:[],spaceWidth:0,spacesIndex:[]},a=!0,s.lines.push(i),s.height+=h},x=p=>p-d>c;for(let p=0;p50&&X("BitmapText",`You have dynamically created ${H} bitmap fonts, this can be inefficient. Try pre installing your font styles using \`BitmapFont.install({name:"style1", style})\``),u.once("destroy",()=>{H--,P.remove(r)}),P.set(r,u)}const i=P.get(r);return(n=i.ensureCharacters)==null||n.call(i,e),i}getLayout(e,t,r=!0){const s=this.getFont(e,t),i=`${e}-${t.styleKey}-${r}`;if(this.measureCache.has(i))return this.measureCache.get(i);const n=D.graphemeSegmenter(e),a=De(n,t,s,r);return this.measureCache.set(i,a),a}measureText(e,t,r=!0){return this.getLayout(e,t,r)}install(...e){var l,d,c,h;let t=e[0];typeof t=="string"&&(t={name:t,style:e[1],chars:(l=e[2])==null?void 0:l.chars,resolution:(d=e[2])==null?void 0:d.resolution,padding:(c=e[2])==null?void 0:c.padding,skipKerning:(h=e[2])==null?void 0:h.skipKerning},w(U,"BitmapFontManager.install(name, style, options) is deprecated, use BitmapFontManager.install({name, style, ...options})"));const r=t==null?void 0:t.name;if(!r)throw new Error("[BitmapFontManager] Property `name` is required.");t={...this.defaultOptions,...t};const s=t.style,i=s instanceof I?s:new I(s),n=t.dynamicFill??this._canUseTintForStyle(i),a=new pe({style:i,overrideFill:n,skipKerning:t.skipKerning,padding:t.padding,resolution:t.resolution,overrideSize:!1,textureStyle:t.textureStyle}),u=Rt(t.chars);return a.ensureCharacters(u.join("")),P.set(`${r}-bitmap`,a),a.once("destroy",()=>P.remove(`${r}-bitmap`)),a}uninstall(e){const t=`${e}-bitmap`,r=P.get(t);r&&r.destroy()}_canUseTintForStyle(e){return!e._stroke&&(!e.dropShadow||e.dropShadow.color===0)&&!e._fill.fill&&e._fill.color===16777215}}const Ut=new Mt;class Oe{constructor(e){this._renderer=e}push(e,t,r){this._renderer.renderPipes.batch.break(r),r.add({renderPipeId:"filter",canBundle:!1,action:"pushFilter",container:t,filterEffect:e})}pop(e,t,r){this._renderer.renderPipes.batch.break(r),r.add({renderPipeId:"filter",action:"popFilter",canBundle:!1})}execute(e){e.action==="pushFilter"?this._renderer.filter.push(e):e.action==="popFilter"&&this._renderer.filter.pop()}destroy(){this._renderer=null}}Oe.extension={type:[g.WebGLPipes,g.WebGPUPipes,g.CanvasPipes],name:"filter"};const ge=new R;function Gt(o,e){e.clear();const t=e.matrix;for(let r=0;r"},uInputPixel:{value:new Float32Array(4),type:"vec4"},uInputClamp:{value:new Float32Array(4),type:"vec4"},uOutputFrame:{value:new Float32Array(4),type:"vec4"},uGlobalFrame:{value:new Float32Array(4),type:"vec4"},uOutputTexture:{value:new Float32Array(4),type:"vec4"}}),this._globalFilterBindGroup=new Se({}),this.renderer=e}get activeBackTexture(){var e;return(e=this._activeFilterData)==null?void 0:e.backTexture}push(e){const t=this.renderer,r=e.filterEffect.filters,s=this._pushFilterData();s.skip=!1,s.filters=r,s.container=e.container,s.outputRenderSurface=t.renderTarget.renderSurface;const i=t.renderTarget.renderTarget.colorTexture.source,n=i.resolution,a=i.antialias;if(r.length===0){s.skip=!0;return}const u=s.bounds;if(this._calculateFilterArea(e,u),this._calculateFilterBounds(s,t.renderTarget.rootViewPort,a,n,1),s.skip)return;const l=this._getPreviousFilterData(),d=this._findFilterResolution(n);let c=0,h=0;l&&(c=l.bounds.minX,h=l.bounds.minY),this._calculateGlobalFrame(s,c,h,d,i.width,i.height),this._setupFilterTextures(s,u,t,l)}generateFilteredTexture({texture:e,filters:t}){const r=this._pushFilterData();this._activeFilterData=r,r.skip=!1,r.filters=t;const s=e.source,i=s.resolution,n=s.antialias;if(t.length===0)return r.skip=!0,e;const a=r.bounds;if(a.addRect(e.frame),this._calculateFilterBounds(r,a.rectangle,n,i,0),r.skip)return e;const u=i;this._calculateGlobalFrame(r,0,0,u,s.width,s.height),r.outputRenderSurface=F.getOptimalTexture(a.width,a.height,r.resolution,r.antialias),r.backTexture=C.EMPTY,r.inputTexture=e,this.renderer.renderTarget.finishRenderPass(),this._applyFiltersToTexture(r,!0);const h=r.outputRenderSurface;return h.source.alphaMode="premultiplied-alpha",h}pop(){const e=this.renderer,t=this._popFilterData();t.skip||(e.globalUniforms.pop(),e.renderTarget.finishRenderPass(),this._activeFilterData=t,this._applyFiltersToTexture(t,!1),t.blendRequired&&F.returnTexture(t.backTexture),F.returnTexture(t.inputTexture))}getBackTexture(e,t,r){const s=e.colorTexture.source._resolution,i=F.getOptimalTexture(t.width,t.height,s,!1);let n=t.minX,a=t.minY;r&&(n-=r.minX,a-=r.minY),n=Math.floor(n*s),a=Math.floor(a*s);const u=Math.ceil(t.width*s),l=Math.ceil(t.height*s);return this.renderer.renderTarget.copyToTexture(e,i,{x:n,y:a},{width:u,height:l},{x:0,y:0}),i}applyFilter(e,t,r,s){const i=this.renderer,n=this._activeFilterData,u=n.outputRenderSurface===r,l=i.renderTarget.rootRenderTarget.colorTexture.source._resolution,d=this._findFilterResolution(l);let c=0,h=0;if(u){const f=this._findPreviousFilterOffset();c=f.x,h=f.y}this._updateFilterUniforms(t,r,n,c,h,d,u,s),this._setupBindGroupsAndRender(e,t,i)}calculateSpriteMatrix(e,t){const r=this._activeFilterData,s=e.set(r.inputTexture._source.width,0,0,r.inputTexture._source.height,r.bounds.minX,r.bounds.minY),i=t.worldTransform.copyTo(R.shared),n=t.renderGroup||t.parentRenderGroup;return n&&n.cacheToLocalTransform&&i.prepend(n.cacheToLocalTransform),i.invert(),s.prepend(i),s.scale(1/t.texture.orig.width,1/t.texture.orig.height),s.translate(t.anchor.x,t.anchor.y),s}destroy(){}_setupBindGroupsAndRender(e,t,r){if(r.renderPipes.uniformBatch){const s=r.renderPipes.uniformBatch.getUboResource(this._filterGlobalUniforms);this._globalFilterBindGroup.setResource(s,0)}else this._globalFilterBindGroup.setResource(this._filterGlobalUniforms,0);this._globalFilterBindGroup.setResource(t.source,1),this._globalFilterBindGroup.setResource(t.source.style,2),e.groups[0]=this._globalFilterBindGroup,r.encoder.draw({geometry:At,shader:e,state:e._state,topology:"triangle-list"}),r.type===ie.WEBGL&&r.renderTarget.finishRenderPass()}_setupFilterTextures(e,t,r,s){if(e.backTexture=C.EMPTY,e.inputTexture=F.getOptimalTexture(t.width,t.height,e.resolution,e.antialias),e.blendRequired){r.renderTarget.finishRenderPass();const i=r.renderTarget.getRenderTarget(e.outputRenderSurface);e.backTexture=this.getBackTexture(i,t,s==null?void 0:s.bounds)}r.renderTarget.bind(e.inputTexture,!0),r.globalUniforms.push({offset:t})}_calculateGlobalFrame(e,t,r,s,i,n){const a=e.globalFrame;a.x=t*s,a.y=r*s,a.width=i*s,a.height=n*s}_updateFilterUniforms(e,t,r,s,i,n,a,u){const l=this._filterGlobalUniforms.uniforms,d=l.uOutputFrame,c=l.uInputSize,h=l.uInputPixel,f=l.uInputClamp,y=l.uGlobalFrame,m=l.uOutputTexture;a?(d[0]=r.bounds.minX-s,d[1]=r.bounds.minY-i):(d[0]=0,d[1]=0),d[2]=e.frame.width,d[3]=e.frame.height,c[0]=e.source.width,c[1]=e.source.height,c[2]=1/c[0],c[3]=1/c[1],h[0]=e.source.pixelWidth,h[1]=e.source.pixelHeight,h[2]=1/h[0],h[3]=1/h[1],f[0]=.5*h[2],f[1]=.5*h[3],f[2]=e.frame.width*c[2]-.5*h[2],f[3]=e.frame.height*c[3]-.5*h[3];const x=this.renderer.renderTarget.rootRenderTarget.colorTexture;y[0]=s*n,y[1]=i*n,y[2]=x.source.width*n,y[3]=x.source.height*n,t instanceof C&&(t.source.resource=null);const p=this.renderer.renderTarget.getRenderTarget(t);this.renderer.renderTarget.bind(t,!!u),t instanceof C?(m[0]=t.frame.width,m[1]=t.frame.height):(m[0]=p.width,m[1]=p.height),m[2]=p.isRoot?-1:1,this._filterGlobalUniforms.update()}_findFilterResolution(e){let t=this._filterStackIndex-1;for(;t>0&&this._filterStack[t].skip;)--t;return t>0&&this._filterStack[t].inputTexture?this._filterStack[t].inputTexture.source._resolution:e}_findPreviousFilterOffset(){let e=0,t=0,r=this._filterStackIndex;for(;r>0;){r--;const s=this._filterStack[r];if(!s.skip){e=s.bounds.minX,t=s.bounds.minY;break}}return{x:e,y:t}}_calculateFilterArea(e,t){if(e.renderables?Gt(e.renderables,t):e.filterEffect.filterArea?(t.clear(),t.addRect(e.filterEffect.filterArea),t.applyMatrix(e.container.worldTransform)):e.container.getFastGlobalBounds(!0,t),e.container){const s=(e.container.renderGroup||e.container.parentRenderGroup).cacheToLocalTransform;s&&t.applyMatrix(s)}}_applyFiltersToTexture(e,t){const r=e.inputTexture,s=e.bounds,i=e.filters;if(this._globalFilterBindGroup.setResource(r.source.style,2),this._globalFilterBindGroup.setResource(e.backTexture.source,3),i.length===1)i[0].apply(this,r,e.outputRenderSurface,t);else{let n=e.inputTexture;const a=F.getOptimalTexture(s.width,s.height,n.source._resolution,!1);let u=a,l=0;for(l=0;l0&&(t--,e=this._filterStack[t],!!e.skip););return e}_pushFilterData(){let e=this._filterStack[this._filterStackIndex];return e||(e=this._filterStack[this._filterStackIndex]=new zt),this._filterStackIndex++,e}}We.extension={type:[g.WebGLSystem,g.WebGPUSystem],name:"filter"};const Ie=class Ee extends se{constructor(...e){let t=e[0]??{};t instanceof Float32Array&&(w(U,"use new MeshGeometry({ positions, uvs, indices }) instead"),t={positions:t,uvs:e[1],indices:e[2]}),t={...Ee.defaultOptions,...t};const r=t.positions||new Float32Array([0,0,1,0,1,1,0,1]);let s=t.uvs;s||(t.positions?s=new Float32Array(r.length):s=new Float32Array([0,0,1,0,1,1,0,1]));const i=t.indices||new Uint32Array([0,1,2,0,2,3]),n=t.shrinkBuffersToFit,a=new O({data:r,label:"attribute-mesh-positions",shrinkToFit:n,usage:B.VERTEX|B.COPY_DST}),u=new O({data:s,label:"attribute-mesh-uvs",shrinkToFit:n,usage:B.VERTEX|B.COPY_DST}),l=new O({data:i,label:"index-mesh-buffer",shrinkToFit:n,usage:B.INDEX|B.COPY_DST});super({attributes:{aPosition:{buffer:a,format:"float32x2",stride:2*4,offset:0},aUV:{buffer:u,format:"float32x2",stride:2*4,offset:0}},indexBuffer:l,topology:t.topology}),this.batchMode="auto"}get positions(){return this.attributes.aPosition.buffer.data}set positions(e){this.attributes.aPosition.buffer.data=e}get uvs(){return this.attributes.aUV.buffer.data}set uvs(e){this.attributes.aUV.buffer.data=e}get indices(){return this.indexBuffer.data}set indices(e){this.indexBuffer.data=e}};Ie.defaultOptions={topology:"triangle-list",shrinkBuffersToFit:!1};let ue=Ie;const me="http://www.w3.org/2000/svg",xe="http://www.w3.org/1999/xhtml";class Le{constructor(){this.svgRoot=document.createElementNS(me,"svg"),this.foreignObject=document.createElementNS(me,"foreignObject"),this.domElement=document.createElementNS(xe,"div"),this.styleElement=document.createElementNS(xe,"style");const{foreignObject:e,svgRoot:t,styleElement:r,domElement:s}=this;e.setAttribute("width","10000"),e.setAttribute("height","10000"),e.style.overflow="hidden",t.appendChild(e),e.appendChild(r),e.appendChild(s),this.image=ne.get().createImage()}destroy(){this.svgRoot.remove(),this.foreignObject.remove(),this.styleElement.remove(),this.domElement.remove(),this.image.src="",this.image.remove(),this.svgRoot=null,this.foreignObject=null,this.styleElement=null,this.domElement=null,this.image=null,this.canvasAndContext=null}}let _e;function kt(o,e,t,r){r||(r=_e||(_e=new Le));const{domElement:s,styleElement:i,svgRoot:n}=r;s.innerHTML=`
${o}
`,s.setAttribute("style","transform-origin: top left; display: inline-block"),t&&(i.textContent=t),document.body.appendChild(n);const a=s.getBoundingClientRect();n.remove();const u=e.padding*2;return{width:a.width-u,height:a.height-u}}class Dt{constructor(){this.batches=[],this.batched=!1}destroy(){this.batches.forEach(e=>{K.return(e)}),this.batches.length=0}}class He{constructor(e,t){this.state=j.for2d(),this.renderer=e,this._adaptor=t,this.renderer.runners.contextChange.add(this)}contextChange(){this._adaptor.contextChange(this.renderer)}validateRenderable(e){const t=e.context,r=!!e._gpuData,s=this.renderer.graphicsContext.updateGpuContext(t);return!!(s.isBatchable||r!==s.isBatchable)}addRenderable(e,t){const r=this.renderer.graphicsContext.updateGpuContext(e.context);e.didViewUpdate&&this._rebuild(e),r.isBatchable?this._addToBatcher(e,t):(this.renderer.renderPipes.batch.break(t),t.add(e))}updateRenderable(e){const r=this._getGpuDataForRenderable(e).batches;for(let s=0;s{const a=K.get(ht);return n.copyTo(a),a.renderable=e,a.roundPixels=i,a})}destroy(){this.renderer=null,this._adaptor.destroy(),this._adaptor=null,this.state=null}}He.extension={type:[g.WebGLPipes,g.WebGPUPipes,g.CanvasPipes],name:"graphics"};const Ve=class Ye extends ue{constructor(...e){super({});let t=e[0]??{};typeof t=="number"&&(w(U,"PlaneGeometry constructor changed please use { width, height, verticesX, verticesY } instead"),t={width:t,height:e[1],verticesX:e[2],verticesY:e[3]}),this.build(t)}build(e){e={...Ye.defaultOptions,...e},this.verticesX=this.verticesX??e.verticesX,this.verticesY=this.verticesY??e.verticesY,this.width=this.width??e.width,this.height=this.height??e.height;const t=this.verticesX*this.verticesY,r=[],s=[],i=[],n=this.verticesX-1,a=this.verticesY-1,u=this.width/n,l=this.height/a;for(let c=0;c"},uColor:{value:new Float32Array([1,1,1,1]),type:"vec4"},uRound:{value:0,type:"f32"}}),this.localUniformsBindGroup=new Se({0:this.localUniforms}),this.renderer=e,this._adaptor=t,this._adaptor.init()}validateRenderable(e){const t=this._getMeshData(e),r=t.batched,s=e.batched;if(t.batched=s,r!==s)return!0;if(s){const i=e._geometry;if(i.indices.length!==t.indexSize||i.positions.length!==t.vertexSize)return t.indexSize=i.indices.length,t.vertexSize=i.positions.length,!0;const n=this._getBatchableMesh(e);return n.texture.uid!==e._texture.uid&&(n._textureMatrixUpdateId=-1),!n._batcher.checkAndUpdateTexture(n,e._texture)}return!1}addRenderable(e,t){var i,n;const r=this.renderer.renderPipes.batch,s=this._getMeshData(e);if(e.didViewUpdate&&(s.indexSize=(i=e._geometry.indices)==null?void 0:i.length,s.vertexSize=(n=e._geometry.positions)==null?void 0:n.length),s.batched){const a=this._getBatchableMesh(e);a.setTexture(e._texture),a.geometry=e._geometry,r.addToBatch(a,t)}else r.break(t),t.add(e)}updateRenderable(e){if(e.batched){const t=this._getBatchableMesh(e);t.setTexture(e._texture),t.geometry=e._geometry,t._batcher.updateElement(t)}}execute(e){if(!e.isRenderable)return;e.state.blendMode=ae(e.groupBlendMode,e.texture._source);const t=this.localUniforms;t.uniforms.uTransformMatrix=e.groupTransform,t.uniforms.uRound=this.renderer._roundPixels|e._roundPixels,t.update(),$(e.groupColorAlpha,t.uniforms.uColor,0),this._adaptor.execute(this,e)}_getMeshData(e){var t,r;return(t=e._gpuData)[r=this.renderer.uid]||(t[r]=new ye),e._gpuData[this.renderer.uid].meshData||this._initMeshData(e)}_initMeshData(e){return e._gpuData[this.renderer.uid].meshData={batched:e.batched,indexSize:0,vertexSize:0},e._gpuData[this.renderer.uid].meshData}_getBatchableMesh(e){var t,r;return(t=e._gpuData)[r=this.renderer.uid]||(t[r]=new ye),e._gpuData[this.renderer.uid].batchableMesh||this._initBatchableMesh(e)}_initBatchableMesh(e){const t=new le;return t.renderable=e,t.setTexture(e._texture),t.transform=e.groupTransform,t.roundPixels=this.renderer._roundPixels|e._roundPixels,e._gpuData[this.renderer.uid].batchableMesh=t,t}destroy(){this.localUniforms=null,this.localUniformsBindGroup=null,this._adaptor.destroy(),this._adaptor=null,this.renderer=null}}Xe.extension={type:[g.WebGLPipes,g.WebGPUPipes,g.CanvasPipes],name:"mesh"};class Wt{execute(e,t){const r=e.state,s=e.renderer,i=t.shader||e.defaultShader;i.resources.uTexture=t.texture._source,i.resources.uniforms=e.localUniforms;const n=s.gl,a=e.getBuffers(t);s.shader.bind(i),s.state.set(r),s.geometry.bind(a.geometry,i.glProgram);const l=a.geometry.indexBuffer.data.BYTES_PER_ELEMENT===2?n.UNSIGNED_SHORT:n.UNSIGNED_INT;n.drawElements(n.TRIANGLES,t.particleChildren.length*6,l,0)}}class It{execute(e,t){const r=e.renderer,s=t.shader||e.defaultShader;s.groups[0]=r.renderPipes.uniformBatch.getUniformBindGroup(e.localUniforms,!0),s.groups[1]=r.texture.getTextureBindGroup(t.texture);const i=e.state,n=e.getBuffers(t);r.encoder.draw({geometry:n.geometry,shader:t.shader||e.defaultShader,state:i,size:t.particleChildren.length*6})}}function be(o,e=null){const t=o*6;if(t>65535?e||(e=new Uint32Array(t)):e||(e=new Uint16Array(t)),e.length!==t)throw new Error(`Out buffer length is incorrect, got ${e.length} and expected ${t}`);for(let r=0,s=0;rthis._size&&(t=!0,this._size=Math.max(e.length,this._size*1.5|0),this.staticAttributeBuffer=new L(this._size*this._staticStride*4*4),this.dynamicAttributeBuffer=new L(this._size*this._dynamicStride*4*4),this.indexBuffer=be(this._size),this.geometry.indexBuffer.setDataWithSize(this.indexBuffer,this.indexBuffer.byteLength,!0));const r=this.dynamicAttributeBuffer;if(this._dynamicUpload(e,r.float32View,r.uint32View),this._dynamicBuffer.setDataWithSize(this.dynamicAttributeBuffer.float32View,e.length*this._dynamicStride*4,!0),t){const s=this.staticAttributeBuffer;this._staticUpload(e,s.float32View,s.uint32View),this._staticBuffer.setDataWithSize(s.float32View,e.length*this._staticStride*4,!0)}}destroy(){this._staticBuffer.destroy(),this._dynamicBuffer.destroy(),this.geometry.destroy()}}function Ht(o){const e=[];for(const t in o){const r=o[t];e.push(t,r.code,r.dynamic?"d":"s")}return e.join("_")}var Vt=`varying vec2 vUV; +varying vec4 vColor; + +uniform sampler2D uTexture; + +void main(void){ + vec4 color = texture2D(uTexture, vUV) * vColor; + gl_FragColor = color; +}`,Yt=`attribute vec2 aVertex; +attribute vec2 aUV; +attribute vec4 aColor; + +attribute vec2 aPosition; +attribute float aRotation; + +uniform mat3 uTranslationMatrix; +uniform float uRound; +uniform vec2 uResolution; +uniform vec4 uColor; + +varying vec2 vUV; +varying vec4 vColor; + +vec2 roundPixels(vec2 position, vec2 targetSize) +{ + return (floor(((position * 0.5 + 0.5) * targetSize) + 0.5) / targetSize) * 2.0 - 1.0; +} + +void main(void){ + float cosRotation = cos(aRotation); + float sinRotation = sin(aRotation); + float x = aVertex.x * cosRotation - aVertex.y * sinRotation; + float y = aVertex.x * sinRotation + aVertex.y * cosRotation; + + vec2 v = vec2(x, y); + v = v + aPosition; + + gl_Position = vec4((uTranslationMatrix * vec3(v, 1.0)).xy, 0.0, 1.0); + + if(uRound == 1.0) + { + gl_Position.xy = roundPixels(gl_Position.xy, uResolution); + } + + vUV = aUV; + vColor = vec4(aColor.rgb * aColor.a, aColor.a) * uColor; +} +`,Te=` +struct ParticleUniforms { + uTranslationMatrix:mat3x3, + uColor:vec4, + uRound:f32, + uResolution:vec2, +}; + +fn roundPixels(position: vec2, targetSize: vec2) -> vec2 +{ + return (floor(((position * 0.5 + 0.5) * targetSize) + 0.5) / targetSize) * 2.0 - 1.0; +} + +@group(0) @binding(0) var uniforms: ParticleUniforms; + +@group(1) @binding(0) var uTexture: texture_2d; +@group(1) @binding(1) var uSampler : sampler; + +struct VSOutput { + @builtin(position) position: vec4, + @location(0) uv : vec2, + @location(1) color : vec4, + }; +@vertex +fn mainVertex( + @location(0) aVertex: vec2, + @location(1) aPosition: vec2, + @location(2) aUV: vec2, + @location(3) aColor: vec4, + @location(4) aRotation: f32, +) -> VSOutput { + + let v = vec2( + aVertex.x * cos(aRotation) - aVertex.y * sin(aRotation), + aVertex.x * sin(aRotation) + aVertex.y * cos(aRotation) + ) + aPosition; + + var position = vec4((uniforms.uTranslationMatrix * vec3(v, 1.0)).xy, 0.0, 1.0); + + if(uniforms.uRound == 1.0) { + position = vec4(roundPixels(position.xy, uniforms.uResolution), position.zw); + } + + let vColor = vec4(aColor.rgb * aColor.a, aColor.a) * uniforms.uColor; + + return VSOutput( + position, + aUV, + vColor, + ); +} + +@fragment +fn mainFragment( + @location(0) uv: vec2, + @location(1) color: vec4, + @builtin(position) position: vec4, +) -> @location(0) vec4 { + + var sample = textureSample(uTexture, uSampler, uv) * color; + + return sample; +}`;class Xt extends oe{constructor(){const e=ft.from({vertex:Yt,fragment:Vt}),t=pt.from({fragment:{source:Te,entryPoint:"mainFragment"},vertex:{source:Te,entryPoint:"mainVertex"}});super({glProgram:e,gpuProgram:t,resources:{uTexture:C.WHITE.source,uSampler:new W({}),uniforms:{uTranslationMatrix:{value:new R,type:"mat3x3"},uColor:{value:new Ce(16777215),type:"vec4"},uRound:{value:1,type:"f32"},uResolution:{value:[0,0],type:"vec2"}}}})}}class Ke{constructor(e,t){this.state=j.for2d(),this.localUniforms=new z({uTranslationMatrix:{value:new R,type:"mat3x3"},uColor:{value:new Float32Array(4),type:"vec4"},uRound:{value:1,type:"f32"},uResolution:{value:[0,0],type:"vec2"}}),this.renderer=e,this.adaptor=t,this.defaultShader=new Xt,this.state=j.for2d()}validateRenderable(e){return!1}addRenderable(e,t){this.renderer.renderPipes.batch.break(t),t.add(e)}getBuffers(e){return e._gpuData[this.renderer.uid]||this._initBuffer(e)}_initBuffer(e){return e._gpuData[this.renderer.uid]=new Lt({size:e.particleChildren.length,properties:e._properties}),e._gpuData[this.renderer.uid]}updateRenderable(e){}execute(e){const t=e.particleChildren;if(t.length===0)return;const r=this.renderer,s=this.getBuffers(e);e.texture||(e.texture=t[0].texture);const i=this.state;s.update(t,e._childrenDirty),e._childrenDirty=!1,i.blendMode=ae(e.blendMode,e.texture._source);const n=this.localUniforms.uniforms,a=n.uTranslationMatrix;e.worldTransform.copyTo(a),a.prepend(r.globalUniforms.globalUniformData.projectionMatrix),n.uResolution=r.globalUniforms.globalUniformData.resolution,n.uRound=r._roundPixels|e._roundPixels,$(e.groupColorAlpha,n.uColor,0),this.adaptor.execute(this,e)}destroy(){this.renderer=null,this.defaultShader&&(this.defaultShader.destroy(),this.defaultShader=null)}}class je extends Ke{constructor(e){super(e,new Wt)}}je.extension={type:[g.WebGLPipes],name:"particle"};class $e extends Ke{constructor(e){super(e,new It)}}$e.extension={type:[g.WebGPUPipes],name:"particle"};const Ne=class qe extends Ot{constructor(e={}){e={...qe.defaultOptions,...e},super({width:e.width,height:e.height,verticesX:4,verticesY:4}),this.update(e)}update(e){var t,r;this.width=e.width??this.width,this.height=e.height??this.height,this._originalWidth=e.originalWidth??this._originalWidth,this._originalHeight=e.originalHeight??this._originalHeight,this._leftWidth=e.leftWidth??this._leftWidth,this._rightWidth=e.rightWidth??this._rightWidth,this._topHeight=e.topHeight??this._topHeight,this._bottomHeight=e.bottomHeight??this._bottomHeight,this._anchorX=(t=e.anchor)==null?void 0:t.x,this._anchorY=(r=e.anchor)==null?void 0:r.y,this.updateUvs(),this.updatePositions()}updatePositions(){const e=this.positions,{width:t,height:r,_leftWidth:s,_rightWidth:i,_topHeight:n,_bottomHeight:a,_anchorX:u,_anchorY:l}=this,d=s+i,c=t>d?1:t/d,h=n+a,f=r>h?1:r/h,y=Math.min(c,f),m=u*t,x=l*r;e[0]=e[8]=e[16]=e[24]=-m,e[2]=e[10]=e[18]=e[26]=s*y-m,e[4]=e[12]=e[20]=e[28]=t-i*y-m,e[6]=e[14]=e[22]=e[30]=t-m,e[1]=e[3]=e[5]=e[7]=-x,e[9]=e[11]=e[13]=e[15]=n*y-x,e[17]=e[19]=e[21]=e[23]=r-a*y-x,e[25]=e[27]=e[29]=e[31]=r-x,this.getBuffer("aPosition").update()}updateUvs(){const e=this.uvs;e[0]=e[8]=e[16]=e[24]=0,e[1]=e[3]=e[5]=e[7]=0,e[6]=e[14]=e[22]=e[30]=1,e[25]=e[27]=e[29]=e[31]=1;const t=1/this._originalWidth,r=1/this._originalHeight;e[2]=e[10]=e[18]=e[26]=t*this._leftWidth,e[9]=e[11]=e[13]=e[15]=r*this._topHeight,e[4]=e[12]=e[20]=e[28]=1-t*this._rightWidth,e[17]=e[19]=e[21]=e[23]=1-r*this._bottomHeight,this.getBuffer("aUV").update()}};Ne.defaultOptions={width:100,height:100,leftWidth:10,topHeight:10,rightWidth:10,bottomHeight:10,originalWidth:100,originalHeight:100};let Kt=Ne;class jt extends le{constructor(){super(),this.geometry=new Kt}destroy(){this.geometry.destroy()}}class Qe{constructor(e){this._renderer=e}addRenderable(e,t){const r=this._getGpuSprite(e);e.didViewUpdate&&this._updateBatchableSprite(e,r),this._renderer.renderPipes.batch.addToBatch(r,t)}updateRenderable(e){const t=this._getGpuSprite(e);e.didViewUpdate&&this._updateBatchableSprite(e,t),t._batcher.updateElement(t)}validateRenderable(e){const t=this._getGpuSprite(e);return!t._batcher.checkAndUpdateTexture(t,e._texture)}_updateBatchableSprite(e,t){t.geometry.update(e),t.setTexture(e._texture)}_getGpuSprite(e){return e._gpuData[this._renderer.uid]||this._initGPUSprite(e)}_initGPUSprite(e){const t=e._gpuData[this._renderer.uid]=new jt,r=t;return r.renderable=e,r.transform=e.groupTransform,r.texture=e._texture,r.roundPixels=this._renderer._roundPixels|e._roundPixels,e.didViewUpdate||this._updateBatchableSprite(e,r),t}destroy(){this._renderer=null}}Qe.extension={type:[g.WebGLPipes,g.WebGPUPipes,g.CanvasPipes],name:"nineSliceSprite"};const $t={name:"tiling-bit",vertex:{header:` + struct TilingUniforms { + uMapCoord:mat3x3, + uClampFrame:vec4, + uClampOffset:vec2, + uTextureTransform:mat3x3, + uSizeAnchor:vec4 + }; + + @group(2) @binding(0) var tilingUniforms: TilingUniforms; + @group(2) @binding(1) var uTexture: texture_2d; + @group(2) @binding(2) var uSampler: sampler; + `,main:` + uv = (tilingUniforms.uTextureTransform * vec3(uv, 1.0)).xy; + + position = (position - tilingUniforms.uSizeAnchor.zw) * tilingUniforms.uSizeAnchor.xy; + `},fragment:{header:` + struct TilingUniforms { + uMapCoord:mat3x3, + uClampFrame:vec4, + uClampOffset:vec2, + uTextureTransform:mat3x3, + uSizeAnchor:vec4 + }; + + @group(2) @binding(0) var tilingUniforms: TilingUniforms; + @group(2) @binding(1) var uTexture: texture_2d; + @group(2) @binding(2) var uSampler: sampler; + `,main:` + + var coord = vUV + ceil(tilingUniforms.uClampOffset - vUV); + coord = (tilingUniforms.uMapCoord * vec3(coord, 1.0)).xy; + var unclamped = coord; + coord = clamp(coord, tilingUniforms.uClampFrame.xy, tilingUniforms.uClampFrame.zw); + + var bias = 0.; + + if(unclamped.x == coord.x && unclamped.y == coord.y) + { + bias = -32.; + } + + outColor = textureSampleBias(uTexture, uSampler, coord, bias); + `}},Nt={name:"tiling-bit",vertex:{header:` + uniform mat3 uTextureTransform; + uniform vec4 uSizeAnchor; + + `,main:` + uv = (uTextureTransform * vec3(aUV, 1.0)).xy; + + position = (position - uSizeAnchor.zw) * uSizeAnchor.xy; + `},fragment:{header:` + uniform sampler2D uTexture; + uniform mat3 uMapCoord; + uniform vec4 uClampFrame; + uniform vec2 uClampOffset; + `,main:` + + vec2 coord = vUV + ceil(uClampOffset - vUV); + coord = (uMapCoord * vec3(coord, 1.0)).xy; + vec2 unclamped = coord; + coord = clamp(coord, uClampFrame.xy, uClampFrame.zw); + + outColor = texture(uTexture, coord, unclamped == coord ? 0.0 : -32.0);// lod-bias very negative to force lod 0 + + `}};let q,Q;class qt extends oe{constructor(){q??(q=Fe({name:"tiling-sprite-shader",bits:[wt,$t,Be]})),Q??(Q=Re({name:"tiling-sprite-shader",bits:[Ct,Nt,Me]}));const e=new z({uMapCoord:{value:new R,type:"mat3x3"},uClampFrame:{value:new Float32Array([0,0,1,1]),type:"vec4"},uClampOffset:{value:new Float32Array([0,0]),type:"vec2"},uTextureTransform:{value:new R,type:"mat3x3"},uSizeAnchor:{value:new Float32Array([100,100,.5,.5]),type:"vec4"}});super({glProgram:Q,gpuProgram:q,resources:{localUniforms:new z({uTransformMatrix:{value:new R,type:"mat3x3"},uColor:{value:new Float32Array([1,1,1,1]),type:"vec4"},uRound:{value:0,type:"f32"}}),tilingUniforms:e,uTexture:C.EMPTY.source,uSampler:C.EMPTY.source.style}})}updateUniforms(e,t,r,s,i,n){const a=this.resources.tilingUniforms,u=n.width,l=n.height,d=n.textureMatrix,c=a.uniforms.uTextureTransform;c.set(r.a*u/e,r.b*u/t,r.c*l/e,r.d*l/t,r.tx/e,r.ty/t),c.invert(),a.uniforms.uMapCoord=d.mapCoord,a.uniforms.uClampFrame=d.uClampFrame,a.uniforms.uClampOffset=d.uClampOffset,a.uniforms.uTextureTransform=c,a.uniforms.uSizeAnchor[0]=e,a.uniforms.uSizeAnchor[1]=t,a.uniforms.uSizeAnchor[2]=s,a.uniforms.uSizeAnchor[3]=i,n&&(this.resources.uTexture=n.source,this.resources.uSampler=n.source.style)}}class Qt extends ue{constructor(){super({positions:new Float32Array([0,0,1,0,1,1,0,1]),uvs:new Float32Array([0,0,1,0,1,1,0,1]),indices:new Uint32Array([0,1,2,0,2,3])})}}function Jt(o,e){const t=o.anchor.x,r=o.anchor.y;e[0]=-t*o.width,e[1]=-r*o.height,e[2]=(1-t)*o.width,e[3]=-r*o.height,e[4]=(1-t)*o.width,e[5]=(1-r)*o.height,e[6]=-t*o.width,e[7]=(1-r)*o.height}function Zt(o,e,t,r){let s=0;const i=o.length/e,n=r.a,a=r.b,u=r.c,l=r.d,d=r.tx,c=r.ty;for(t*=e;s, + uTransformMatrix:mat3x3, + uDistance: f32, + uRound:f32, + } + + @group(2) @binding(0) var localUniforms : LocalUniforms; + `,main:` + vColor *= localUniforms.uColor; + modelMatrix *= localUniforms.uTransformMatrix; + `,end:` + if(localUniforms.uRound == 1) + { + vPosition = vec4(roundPixels(vPosition.xy, globalUniforms.uResolution), vPosition.zw); + } + `},fragment:{header:` + struct LocalUniforms { + uColor:vec4, + uTransformMatrix:mat3x3, + uDistance: f32 + } + + @group(2) @binding(0) var localUniforms : LocalUniforms; + `,main:` + outColor = vec4(calculateMSDFAlpha(outColor, localUniforms.uColor, localUniforms.uDistance)); + `}},sr={name:"local-uniform-msdf-bit",vertex:{header:` + uniform mat3 uTransformMatrix; + uniform vec4 uColor; + uniform float uRound; + `,main:` + vColor *= uColor; + modelMatrix *= uTransformMatrix; + `,end:` + if(uRound == 1.) + { + gl_Position.xy = roundPixels(gl_Position.xy, uResolution); + } + `},fragment:{header:` + uniform float uDistance; + `,main:` + outColor = vec4(calculateMSDFAlpha(outColor, vColor, uDistance)); + `}},ir={name:"msdf-bit",fragment:{header:` + fn calculateMSDFAlpha(msdfColor:vec4, shapeColor:vec4, distance:f32) -> f32 { + + // MSDF + var median = msdfColor.r + msdfColor.g + msdfColor.b - + min(msdfColor.r, min(msdfColor.g, msdfColor.b)) - + max(msdfColor.r, max(msdfColor.g, msdfColor.b)); + + // SDF + median = min(median, msdfColor.a); + + var screenPxDistance = distance * (median - 0.5); + var alpha = clamp(screenPxDistance + 0.5, 0.0, 1.0); + if (median < 0.01) { + alpha = 0.0; + } else if (median > 0.99) { + alpha = 1.0; + } + + // Gamma correction for coverage-like alpha + var luma: f32 = dot(shapeColor.rgb, vec3(0.299, 0.587, 0.114)); + var gamma: f32 = mix(1.0, 1.0 / 2.2, luma); + var coverage: f32 = pow(shapeColor.a * alpha, gamma); + + return coverage; + + } + `}},nr={name:"msdf-bit",fragment:{header:` + float calculateMSDFAlpha(vec4 msdfColor, vec4 shapeColor, float distance) { + + // MSDF + float median = msdfColor.r + msdfColor.g + msdfColor.b - + min(msdfColor.r, min(msdfColor.g, msdfColor.b)) - + max(msdfColor.r, max(msdfColor.g, msdfColor.b)); + + // SDF + median = min(median, msdfColor.a); + + float screenPxDistance = distance * (median - 0.5); + float alpha = clamp(screenPxDistance + 0.5, 0.0, 1.0); + + if (median < 0.01) { + alpha = 0.0; + } else if (median > 0.99) { + alpha = 1.0; + } + + // Gamma correction for coverage-like alpha + float luma = dot(shapeColor.rgb, vec3(0.299, 0.587, 0.114)); + float gamma = mix(1.0, 1.0 / 2.2, luma); + float coverage = pow(shapeColor.a * alpha, gamma); + + return coverage; + } + `}};let J,Z;class ar extends oe{constructor(e){const t=new z({uColor:{value:new Float32Array([1,1,1,1]),type:"vec4"},uTransformMatrix:{value:new R,type:"mat3x3"},uDistance:{value:4,type:"f32"},uRound:{value:0,type:"f32"}});J??(J=Fe({name:"sdf-shader",bits:[gt,mt(e),rr,ir,Be]})),Z??(Z=Re({name:"sdf-shader",bits:[xt,_t(e),sr,nr,Me]})),super({glProgram:Z,gpuProgram:J,resources:{localUniforms:t,batchSamplers:yt(e)}})}}class or extends bt{destroy(){this.context.customShader&&this.context.customShader.destroy(),super.destroy()}}class Ze{constructor(e){this._renderer=e}validateRenderable(e){const t=this._getGpuBitmapText(e);return this._renderer.renderPipes.graphics.validateRenderable(t)}addRenderable(e,t){const r=this._getGpuBitmapText(e);we(e,r),e._didTextUpdate&&(e._didTextUpdate=!1,this._updateContext(e,r)),this._renderer.renderPipes.graphics.addRenderable(r,t),r.context.customShader&&this._updateDistanceField(e)}updateRenderable(e){const t=this._getGpuBitmapText(e);we(e,t),this._renderer.renderPipes.graphics.updateRenderable(t),t.context.customShader&&this._updateDistanceField(e)}_updateContext(e,t){const{context:r}=t,s=Ut.getFont(e.text,e._style);r.clear(),s.distanceField.type!=="none"&&(r.customShader||(r.customShader=new ar(this._renderer.limits.maxBatchableTextures)));const i=D.graphemeSegmenter(e.text),n=e._style;let a=s.baseLineOffset;const u=De(i,n,s,!0),l=n.padding,d=u.scale;let c=u.width,h=u.height+u.offsetY;n._stroke&&(c+=n._stroke.width/d,h+=n._stroke.width/d),r.translate(-e._anchor._x*c-l,-e._anchor._y*h-l).scale(d,d);const f=s.applyFillAsTint?n._fill.color:16777215;let y=s.fontMetrics.fontSize,m=s.lineHeight;n.lineHeight&&(y=n.fontSize/d,m=n.lineHeight/d);let x=(m-y)/2;x-s.baseLineOffset<0&&(x=0);for(let p=0;p{console.error(i)}),e._didTextUpdate=!1,re(r,e)}this._renderer.renderPipes.batch.addToBatch(r,t)}updateRenderable(e){const t=this._getGpuText(e);t._batcher.updateElement(t)}async _updateGpuText(e){e._didTextUpdate=!1;const t=this._getGpuText(e);if(t.generatingTexture)return;const r=t.texturePromise;t.texturePromise=null,t.generatingTexture=!0,e._resolution=e._autoResolution?this._renderer.resolution:e.resolution;let s=this._renderer.htmlText.getTexturePromise(e);r&&(s=s.finally(()=>{this._renderer.htmlText.decreaseReferenceCount(t.currentKey),this._renderer.htmlText.returnTexturePromise(r)})),t.texturePromise=s,t.currentKey=e.styleKey,t.texture=await s;const i=e.renderGroup||e.parentRenderGroup;i&&(i.structureDidChange=!0),t.generatingTexture=!1,re(t,e)}_getGpuText(e){return e._gpuData[this._renderer.uid]||this.initGpuText(e)}initGpuText(e){const t=new ur(this._renderer);return t.renderable=e,t.transform=e.groupTransform,t.texture=C.EMPTY,t.bounds={minX:0,maxX:1,minY:0,maxY:0},t.roundPixels=this._renderer._roundPixels|e._roundPixels,e._resolution=e._autoResolution?this._renderer.resolution:e.resolution,e._gpuData[this._renderer.uid]=t,t}destroy(){this._renderer=null}}et.extension={type:[g.WebGLPipes,g.WebGPUPipes,g.CanvasPipes],name:"htmlText"};function lr(){const{userAgent:o}=ne.get().getNavigator();return/^((?!chrome|android).)*safari/i.test(o)}const cr=new Pe;function tt(o,e,t,r){const s=cr;s.minX=0,s.minY=0,s.maxX=o.width/r|0,s.maxY=o.height/r|0;const i=F.getOptimalTexture(s.width,s.height,r,!1);return i.source.uploadMethodId="image",i.source.resource=o,i.source.alphaMode="premultiply-alpha-on-upload",i.frame.width=e/r,i.frame.height=t/r,i.source.emit("update",i.source),i.updateUvs(),i}function dr(o,e){const t=e.fontFamily,r=[],s={},i=/font-family:([^;"\s]+)/g,n=o.match(i);function a(u){s[u]||(r.push(u),s[u]=!0)}if(Array.isArray(t))for(let u=0;u{const l=u.split(":")[1].trim();a(l)});for(const u in e.tagStyles){const l=e.tagStyles[u].fontFamily;a(l)}return r}async function hr(o){const t=await(await ne.get().fetch(o)).blob(),r=new FileReader;return await new Promise((i,n)=>{r.onloadend=()=>i(r.result),r.onerror=n,r.readAsDataURL(t)})}async function fr(o,e){const t=await hr(e);return`@font-face { + font-family: "${o.fontFamily}"; + font-weight: ${o.fontWeight}; + font-style: ${o.fontStyle}; + src: url('${t}'); + }`}const ee=new Map;async function pr(o){const e=o.filter(t=>P.has(`${t}-and-url`)).map(t=>{if(!ee.has(t)){const{entries:r}=P.get(`${t}-and-url`),s=[];r.forEach(i=>{const n=i.url,u=i.faces.map(l=>({weight:l.weight,style:l.style}));s.push(...u.map(l=>fr({fontWeight:l.weight,fontStyle:l.style,fontFamily:t},n)))}),ee.set(t,Promise.all(s).then(i=>i.join(` +`)))}return ee.get(t)});return(await Promise.all(e)).join(` +`)}function gr(o,e,t,r,s){const{domElement:i,styleElement:n,svgRoot:a}=s;i.innerHTML=`
${o}
`,i.setAttribute("style",`transform: scale(${t});transform-origin: top left; display: inline-block`),n.textContent=r;const{width:u,height:l}=s.image;return a.setAttribute("width",u.toString()),a.setAttribute("height",l.toString()),new XMLSerializer().serializeToString(a)}function mr(o,e){const t=Y.getOptimalCanvasAndContext(o.width,o.height,e),{context:r}=t;return r.clearRect(0,0,o.width,o.height),r.drawImage(o,0,0),t}function xr(o,e,t){return new Promise(async r=>{t&&await new Promise(s=>setTimeout(s,100)),o.onload=()=>{r()},o.src=`data:image/svg+xml;charset=utf8,${encodeURIComponent(e)}`,o.crossOrigin="anonymous"})}class rt{constructor(e){this._activeTextures={},this._renderer=e,this._createCanvas=e.type===ie.WEBGPU}getTexture(e){return this.getTexturePromise(e)}getManagedTexture(e){const t=e.styleKey;if(this._activeTextures[t])return this._increaseReferenceCount(t),this._activeTextures[t].promise;const r=this._buildTexturePromise(e).then(s=>(this._activeTextures[t].texture=s,s));return this._activeTextures[t]={texture:null,promise:r,usageCount:1},r}getReferenceCount(e){var t;return((t=this._activeTextures[e])==null?void 0:t.usageCount)??null}_increaseReferenceCount(e){this._activeTextures[e].usageCount++}decreaseReferenceCount(e){const t=this._activeTextures[e];t&&(t.usageCount--,t.usageCount===0&&(t.texture?this._cleanUp(t.texture):t.promise.then(r=>{t.texture=r,this._cleanUp(t.texture)}).catch(()=>{X("HTMLTextSystem: Failed to clean texture")}),this._activeTextures[e]=null))}getTexturePromise(e){return this._buildTexturePromise(e)}async _buildTexturePromise(e){const{text:t,style:r,resolution:s,textureStyle:i}=e,n=K.get(Le),a=dr(t,r),u=await pr(a),l=kt(t,r,u,n),d=Math.ceil(Math.ceil(Math.max(1,l.width)+r.padding*2)*s),c=Math.ceil(Math.ceil(Math.max(1,l.height)+r.padding*2)*s),h=n.image,f=2;h.width=(d|0)+f,h.height=(c|0)+f;const y=gr(t,r,s,u,n);await xr(h,y,lr()&&a.length>0);const m=h;let x;this._createCanvas&&(x=mr(h,s));const p=tt(x?x.canvas:m,h.width-f,h.height-f,s);return i&&(p.source.style=i),this._createCanvas&&(this._renderer.texture.initSource(p.source),Y.returnCanvasAndContext(x)),K.return(n),p}returnTexturePromise(e){e.then(t=>{this._cleanUp(t)}).catch(()=>{X("HTMLTextSystem: Failed to clean texture")})}_cleanUp(e){F.returnTexture(e,!0),e.source.resource=null,e.source.uploadMethodId="unknown"}destroy(){this._renderer=null;for(const e in this._activeTextures)this._activeTextures[e]&&this.returnTexturePromise(this._activeTextures[e].promise);this._activeTextures=null}}rt.extension={type:[g.WebGLSystem,g.WebGPUSystem,g.CanvasSystem],name:"htmlText"};class _r extends Ue{constructor(e){super(),this._renderer=e,e.runners.resolutionChange.add(this)}resolutionChange(){const e=this.renderable;e._autoResolution&&e.onViewUpdate()}destroy(){const{canvasText:e}=this._renderer;e.getReferenceCount(this.currentKey)>0?e.decreaseReferenceCount(this.currentKey):this.texture&&e.returnTexture(this.texture),this._renderer.runners.resolutionChange.remove(this),this._renderer=null}}class st{constructor(e){this._renderer=e}validateRenderable(e){const t=this._getGpuText(e),r=e.styleKey;return t.currentKey!==r?!0:e._didTextUpdate}addRenderable(e,t){const r=this._getGpuText(e);if(e._didTextUpdate){const s=e._autoResolution?this._renderer.resolution:e.resolution;(r.currentKey!==e.styleKey||e.resolution!==s)&&this._updateGpuText(e),e._didTextUpdate=!1}this._renderer.renderPipes.batch.addToBatch(r,t)}updateRenderable(e){const t=this._getGpuText(e);t._batcher.updateElement(t)}_updateGpuText(e){const t=this._getGpuText(e);t.texture&&this._renderer.canvasText.decreaseReferenceCount(t.currentKey),e._resolution=e._autoResolution?this._renderer.resolution:e.resolution,t.texture=this._renderer.canvasText.getManagedTexture(e),t.currentKey=e.styleKey,re(t,e)}_getGpuText(e){return e._gpuData[this._renderer.uid]||this.initGpuText(e)}initGpuText(e){const t=new _r(this._renderer);return t.currentKey="--",t.renderable=e,t.transform=e.groupTransform,t.bounds={minX:0,maxX:1,minY:0,maxY:0},t.roundPixels=this._renderer._roundPixels|e._roundPixels,e._gpuData[this._renderer.uid]=t,t}destroy(){this._renderer=null}}st.extension={type:[g.WebGLPipes,g.WebGPUPipes,g.CanvasPipes],name:"text"};class it{constructor(e){this._activeTextures={},this._renderer=e}getTexture(e,t,r,s){typeof e=="string"&&(w("8.0.0","CanvasTextSystem.getTexture: Use object TextOptions instead of separate arguments"),e={text:e,style:r,resolution:t}),e.style instanceof I||(e.style=new I(e.style)),e.textureStyle instanceof W||(e.textureStyle=new W(e.textureStyle)),typeof e.text!="string"&&(e.text=e.text.toString());const{text:i,style:n,textureStyle:a}=e,u=e.resolution??this._renderer.resolution,{frame:l,canvasAndContext:d}=N.getCanvasAndContext({text:i,style:n,resolution:u}),c=tt(d.canvas,l.width,l.height,u);if(a&&(c.source.style=a),n.trim&&(l.pad(n.padding),c.frame.copyFrom(l),c.frame.scale(1/u),c.updateUvs()),n.filters){const h=this._applyFilters(c,n.filters);return this.returnTexture(c),N.returnCanvasAndContext(d),h}return this._renderer.texture.initSource(c._source),N.returnCanvasAndContext(d),c}returnTexture(e){const t=e.source;t.resource=null,t.uploadMethodId="unknown",t.alphaMode="no-premultiply-alpha",F.returnTexture(e,!0)}renderTextToCanvas(){w("8.10.0","CanvasTextSystem.renderTextToCanvas: no longer supported, use CanvasTextSystem.getTexture instead")}getManagedTexture(e){e._resolution=e._autoResolution?this._renderer.resolution:e.resolution;const t=e.styleKey;if(this._activeTextures[t])return this._increaseReferenceCount(t),this._activeTextures[t].texture;const r=this.getTexture({text:e.text,style:e.style,resolution:e._resolution,textureStyle:e.textureStyle});return this._activeTextures[t]={texture:r,usageCount:1},r}decreaseReferenceCount(e){const t=this._activeTextures[e];t.usageCount--,t.usageCount===0&&(this.returnTexture(t.texture),this._activeTextures[e]=null)}getReferenceCount(e){var t;return((t=this._activeTextures[e])==null?void 0:t.usageCount)??0}_increaseReferenceCount(e){this._activeTextures[e].usageCount++}_applyFilters(e,t){const r=this._renderer.renderTarget.renderTarget,s=this._renderer.filter.generateFilteredTexture({texture:e,filters:t});return this._renderer.renderTarget.bind(r,!1),s}destroy(){this._renderer=null;for(const e in this._activeTextures)this._activeTextures[e]&&this.returnTexture(this._activeTextures[e].texture);this._activeTextures=null}}it.extension={type:[g.WebGLSystem,g.WebGPUSystem,g.CanvasSystem],name:"canvasText"};T.add(Ge);T.add(Ae);T.add(He);T.add(Tt);T.add(Xe);T.add(je);T.add($e);T.add(it);T.add(st);T.add(Ze);T.add(rt);T.add(et);T.add(Je);T.add(Qe);T.add(We);T.add(Oe); diff --git a/dist/demo-data/chirp-ramp.csv b/dist/demo-data/chirp-ramp.csv new file mode 100644 index 0000000..5e81c10 --- /dev/null +++ b/dist/demo-data/chirp-ramp.csv @@ -0,0 +1,47 @@ +time_ms,value +0,-0.04 +120,0.05 +240,0.11 +360,0.07 +480,-0.03 +600,-0.17 +720,-0.26 +840,-0.22 +960,-0.04 +1080,0.23 +1200,0.48 +1320,0.57 +1440,0.38 +1560,-0.01 +1680,-0.43 +1800,-0.67 +1920,-0.55 +2040,-0.07 +2160,0.53 +2280,0.89 +2400,0.76 +2520,0.16 +2640,-0.61 +2760,-1.01 +2880,-0.78 +3000,0.02 +3120,0.87 +3240,1.18 +3360,0.75 +3480,-0.21 +3600,-1.04 +3720,-1.21 +3840,-0.44 +3960,0.63 +4080,1.28 +4200,1.05 +4320,0.01 +4440,-1.01 +4560,-1.34 +4680,-0.69 +4800,0.47 +4920,1.31 +5040,1.26 +5160,0.31 +5280,-0.92 +5400,-1.43 diff --git a/dist/demo-data/step-bursts.csv b/dist/demo-data/step-bursts.csv new file mode 100644 index 0000000..e9dbc3e --- /dev/null +++ b/dist/demo-data/step-bursts.csv @@ -0,0 +1,42 @@ +time_ms,value +0,0.0 +200,0.0 +400,0.0 +600,0.4 +800,0.8 +1000,1.2 +1200,1.2 +1400,1.2 +1600,0.3 +1800,-0.2 +2000,-0.7 +2200,-1.1 +2400,-1.1 +2600,-0.5 +2800,0.1 +3000,0.6 +3200,1.0 +3400,0.5 +3600,-0.4 +3800,-1.0 +4000,-0.6 +4200,0.2 +4400,0.7 +4600,1.1 +4800,0.9 +5000,0.1 +5200,-0.8 +5400,-1.3 +5600,-0.9 +5800,-0.1 +6000,0.8 +6200,1.4 +6400,1.1 +6600,0.0 +6800,-0.9 +7000,-1.4 +7200,-1.0 +7400,-0.2 +7600,0.5 +7800,0.9 +8000,0.0 diff --git a/dist/demo-data/telemetry-sweep.csv b/dist/demo-data/telemetry-sweep.csv new file mode 100644 index 0000000..8c7d6e3 --- /dev/null +++ b/dist/demo-data/telemetry-sweep.csv @@ -0,0 +1,42 @@ +time_ms,value +0,0.12 +150,0.18 +300,0.31 +450,0.44 +600,0.52 +750,0.68 +900,0.83 +1050,0.96 +1200,1.04 +1350,1.08 +1500,1.01 +1650,0.92 +1800,0.77 +1950,0.58 +2100,0.34 +2250,0.12 +2400,-0.08 +2550,-0.22 +2700,-0.35 +2850,-0.48 +3000,-0.59 +3150,-0.66 +3300,-0.72 +3450,-0.64 +3600,-0.49 +3750,-0.27 +3900,-0.02 +4050,0.24 +4200,0.46 +4350,0.67 +4500,0.81 +4650,0.9 +4800,0.95 +4950,0.88 +5100,0.75 +5250,0.54 +5400,0.29 +5550,0.03 +5700,-0.2 +5850,-0.37 +6000,-0.48 diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000..434a873 --- /dev/null +++ b/dist/index.html @@ -0,0 +1,13 @@ + + + + + + TimePlot + + + + +
+ + diff --git a/doc/ui.md.uxf b/doc/ui.md.uxf new file mode 100644 index 0000000..44aef25 --- /dev/null +++ b/doc/ui.md.uxf @@ -0,0 +1,22 @@ + + + 10 + + UMLClass + + 60 + 130 + 590 + 230 + + Graph +-- +Unit-by-unit (unit description) (what known values/units is it relating?) + +each axis has to be some state attribute that has a value at some instant in time within the system, such as time, cursor position. + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..76e8b87 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + TimePlot + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7ce7bea --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1071 @@ +{ + "name": "web-timeplot", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web-timeplot", + "version": "0.1.0", + "dependencies": { + "pixi.js": "^8.0.0", + "ws": "^8.20.0" + }, + "devDependencies": { + "vite": "^5.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@pixi/colord": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", + "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", + "integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz", + "integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz", + "integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz", + "integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz", + "integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz", + "integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz", + "integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz", + "integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz", + "integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz", + "integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz", + "integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz", + "integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz", + "integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz", + "integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz", + "integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz", + "integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz", + "integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz", + "integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz", + "integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz", + "integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz", + "integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz", + "integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", + "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==", + "license": "MIT" + }, + "node_modules/@types/earcut": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-3.0.0.tgz", + "integrity": "sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webgpu/types": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.65.tgz", + "integrity": "sha512-cYrHab4d6wuVvDW5tdsfI6/o6vcLMDe6w2Citd1oS51Xxu2ycLCnVo4fqwujfKWijrZMInTJIKcXxteoy21nVA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gifuct-js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz", + "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==", + "license": "MIT", + "dependencies": { + "js-binary-schema-parser": "^2.0.3" + } + }, + "node_modules/ismobilejs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", + "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==", + "license": "MIT" + }, + "node_modules/js-binary-schema-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz", + "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pixi.js": { + "version": "8.13.2", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.13.2.tgz", + "integrity": "sha512-9KVGZ4a99TA5SwUEWs9m5gliX6XUCS1aGc/DOPsXxpqLMDRa+FhzpT5ao9z1UwLYJkSvt3rcQs+aZXECBHSSHg==", + "license": "MIT", + "dependencies": { + "@pixi/colord": "^2.9.6", + "@types/css-font-loading-module": "^0.0.12", + "@types/earcut": "^3.0.0", + "@webgpu/types": "^0.1.40", + "@xmldom/xmldom": "^0.8.10", + "earcut": "^3.0.2", + "eventemitter3": "^5.0.1", + "gifuct-js": "^2.1.2", + "ismobilejs": "^1.1.1", + "parse-svg-path": "^0.1.2", + "tiny-lru": "^11.4.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", + "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.3", + "@rollup/rollup-android-arm64": "4.52.3", + "@rollup/rollup-darwin-arm64": "4.52.3", + "@rollup/rollup-darwin-x64": "4.52.3", + "@rollup/rollup-freebsd-arm64": "4.52.3", + "@rollup/rollup-freebsd-x64": "4.52.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", + "@rollup/rollup-linux-arm-musleabihf": "4.52.3", + "@rollup/rollup-linux-arm64-gnu": "4.52.3", + "@rollup/rollup-linux-arm64-musl": "4.52.3", + "@rollup/rollup-linux-loong64-gnu": "4.52.3", + "@rollup/rollup-linux-ppc64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-musl": "4.52.3", + "@rollup/rollup-linux-s390x-gnu": "4.52.3", + "@rollup/rollup-linux-x64-gnu": "4.52.3", + "@rollup/rollup-linux-x64-musl": "4.52.3", + "@rollup/rollup-openharmony-arm64": "4.52.3", + "@rollup/rollup-win32-arm64-msvc": "4.52.3", + "@rollup/rollup-win32-ia32-msvc": "4.52.3", + "@rollup/rollup-win32-x64-gnu": "4.52.3", + "@rollup/rollup-win32-x64-msvc": "4.52.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tiny-lru": { + "version": "11.4.5", + "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.5.tgz", + "integrity": "sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/vite": { + "version": "5.4.20", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", + "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9f4220f --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "web-timeplot", + "version": "0.1.0", + "description": "PixiJS waterfall display with WebGPU/WebGL", + "type": "module", + "scripts": { + "dev": "vite --host", + "build": "vite build", + "preview": "vite preview", + "ws:demo": "node ./scripts/demo-websocket-server.mjs" + }, + "devDependencies": { + "vite": "^5.0.0" + }, + "dependencies": { + "pixi.js": "^8.0.0", + "ws": "^8.20.0" + } +} diff --git a/public/demo-data/chirp-ramp.csv b/public/demo-data/chirp-ramp.csv new file mode 100644 index 0000000..5e81c10 --- /dev/null +++ b/public/demo-data/chirp-ramp.csv @@ -0,0 +1,47 @@ +time_ms,value +0,-0.04 +120,0.05 +240,0.11 +360,0.07 +480,-0.03 +600,-0.17 +720,-0.26 +840,-0.22 +960,-0.04 +1080,0.23 +1200,0.48 +1320,0.57 +1440,0.38 +1560,-0.01 +1680,-0.43 +1800,-0.67 +1920,-0.55 +2040,-0.07 +2160,0.53 +2280,0.89 +2400,0.76 +2520,0.16 +2640,-0.61 +2760,-1.01 +2880,-0.78 +3000,0.02 +3120,0.87 +3240,1.18 +3360,0.75 +3480,-0.21 +3600,-1.04 +3720,-1.21 +3840,-0.44 +3960,0.63 +4080,1.28 +4200,1.05 +4320,0.01 +4440,-1.01 +4560,-1.34 +4680,-0.69 +4800,0.47 +4920,1.31 +5040,1.26 +5160,0.31 +5280,-0.92 +5400,-1.43 diff --git a/public/demo-data/step-bursts.csv b/public/demo-data/step-bursts.csv new file mode 100644 index 0000000..e9dbc3e --- /dev/null +++ b/public/demo-data/step-bursts.csv @@ -0,0 +1,42 @@ +time_ms,value +0,0.0 +200,0.0 +400,0.0 +600,0.4 +800,0.8 +1000,1.2 +1200,1.2 +1400,1.2 +1600,0.3 +1800,-0.2 +2000,-0.7 +2200,-1.1 +2400,-1.1 +2600,-0.5 +2800,0.1 +3000,0.6 +3200,1.0 +3400,0.5 +3600,-0.4 +3800,-1.0 +4000,-0.6 +4200,0.2 +4400,0.7 +4600,1.1 +4800,0.9 +5000,0.1 +5200,-0.8 +5400,-1.3 +5600,-0.9 +5800,-0.1 +6000,0.8 +6200,1.4 +6400,1.1 +6600,0.0 +6800,-0.9 +7000,-1.4 +7200,-1.0 +7400,-0.2 +7600,0.5 +7800,0.9 +8000,0.0 diff --git a/public/demo-data/telemetry-sweep.csv b/public/demo-data/telemetry-sweep.csv new file mode 100644 index 0000000..8c7d6e3 --- /dev/null +++ b/public/demo-data/telemetry-sweep.csv @@ -0,0 +1,42 @@ +time_ms,value +0,0.12 +150,0.18 +300,0.31 +450,0.44 +600,0.52 +750,0.68 +900,0.83 +1050,0.96 +1200,1.04 +1350,1.08 +1500,1.01 +1650,0.92 +1800,0.77 +1950,0.58 +2100,0.34 +2250,0.12 +2400,-0.08 +2550,-0.22 +2700,-0.35 +2850,-0.48 +3000,-0.59 +3150,-0.66 +3300,-0.72 +3450,-0.64 +3600,-0.49 +3750,-0.27 +3900,-0.02 +4050,0.24 +4200,0.46 +4350,0.67 +4500,0.81 +4650,0.9 +4800,0.95 +4950,0.88 +5100,0.75 +5250,0.54 +5400,0.29 +5550,0.03 +5700,-0.2 +5850,-0.37 +6000,-0.48 diff --git a/sample_data.txt b/sample_data.txt new file mode 100644 index 0000000..25a57e7 --- /dev/null +++ b/sample_data.txt @@ -0,0 +1,30 @@ +0.1 +0.5 +0.8 +1.2 +1.5 +1.1 +0.9 +0.6 +0.3 +0.7 +1.0 +1.4 +1.8 +2.0 +1.7 +1.3 +0.8 +0.4 +0.2 +0.6 +0.9 +1.3 +1.6 +1.9 +1.4 +1.0 +0.5 +0.1 +0.3 +0.7 \ No newline at end of file diff --git a/scripts/demo-websocket-server.mjs b/scripts/demo-websocket-server.mjs new file mode 100644 index 0000000..1bee865 --- /dev/null +++ b/scripts/demo-websocket-server.mjs @@ -0,0 +1,131 @@ +import { WebSocketServer } from 'ws'; + +const port = Number(process.env.PORT || 8080); +const profile = process.env.TIMEPLOT_PROFILE || 'telemetry'; +const sendIntervalMs = Number(process.env.TIMEPLOT_INTERVAL_MS || 100); +const logEvery = Number(process.env.TIMEPLOT_LOG_EVERY || 10); + +const wss = new WebSocketServer({ port }); +const startedAt = Date.now(); +let sampleIndex = 0; +let activeClientCount = 0; + +function log(message, details = '') { + const timestamp = new Date().toISOString(); + if (details) { + console.log(`[timeplot-ws ${timestamp}] ${message} ${details}`); + return; + } + + console.log(`[timeplot-ws ${timestamp}] ${message}`); +} + +function sampleTelemetry(seconds) { + return Math.sin(seconds * 2.2) + 0.35 * Math.cos(seconds * 6.4 + Math.sin(seconds * 0.8)) + 0.15 * Math.sin(seconds * 0.33); +} + +function sampleChirp(seconds) { + return 0.7 * Math.sin(seconds * seconds * 1.4) + 0.3 * Math.sin(seconds * 7.5); +} + +function sampleSteps(seconds) { + const phase = Math.floor((seconds % 8) / 1.0); + return [0, 0.4, 0.9, 1.2, 0.2, -0.6, -1.0, 0.3][phase] ?? 0; +} + +function sampleBurst(seconds) { + const burstPhase = (seconds % 6) - 1.5; + const burst = Math.sin(seconds * 9.5) * Math.exp(-(burstPhase ** 2) * 0.8); + return 0.45 * Math.sin(seconds * 2.1) + burst; +} + +function sampleValue(seconds) { + switch (profile) { + case 'chirp': + return sampleChirp(seconds); + case 'steps': + return sampleSteps(seconds); + case 'burst': + return sampleBurst(seconds); + case 'telemetry': + default: + return sampleTelemetry(seconds); + } +} + +function buildMessage() { + const timestampMs = Date.now() - startedAt; + const seconds = timestampMs / 1000; + sampleIndex += 1; + + return { + timestampMs, + value: Number(sampleValue(seconds).toFixed(6)), + sequence: sampleIndex, + profile, + }; +} + +const interval = setInterval(() => { + const message = buildMessage(); + const payload = JSON.stringify(message); + let sentCount = 0; + + for (const client of wss.clients) { + if (client.readyState === client.OPEN) { + client.send(payload); + sentCount += 1; + } + } + + if (message.sequence === 1 || (logEvery > 0 && message.sequence % logEvery === 0)) { + log( + 'broadcast', + `seq=${message.sequence} clients=${sentCount} timestampMs=${message.timestampMs} value=${message.value}`, + ); + } +}, sendIntervalMs); + +wss.on('connection', (socket, request) => { + const clientAddress = request.socket.remoteAddress || 'unknown'; + activeClientCount += 1; + log('client connected', `from=${clientAddress} activeClients=${activeClientCount}`); + + socket.send(JSON.stringify({ + timestampMs: 0, + value: 0, + sequence: 0, + profile, + message: 'connected', + })); + + socket.on('error', (error) => { + log('client error', `from=${clientAddress} error=${error.message}`); + }); + + socket.on('close', () => { + activeClientCount = Math.max(0, activeClientCount - 1); + log('client disconnected', `from=${clientAddress} activeClients=${activeClientCount}`); + }); +}); + +wss.on('error', (error) => { + log('server error', error.message); +}); + +wss.on('listening', () => { + log('listening', `url=ws://localhost:${port}`); + log('config', `profile=${profile} intervalMs=${sendIntervalMs} logEvery=${logEvery}`); +}); + +function shutdown() { + log('shutdown requested', `activeClients=${activeClientCount}`); + clearInterval(interval); + wss.close(() => { + log('server stopped'); + process.exit(0); + }); +} + +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); diff --git a/src/app/create-app.js b/src/app/create-app.js new file mode 100644 index 0000000..4f4f0fc --- /dev/null +++ b/src/app/create-app.js @@ -0,0 +1,449 @@ +import { EventBus } from '../core/event-bus.js'; +import { Store, createInitialState } from '../core/store.js'; +import { TimeController } from '../core/time-controller.js'; +import { PlotBuffer } from '../plot/plot-buffer.js'; +import { TimeplotView } from '../plot/timeplot-view.js'; +import { SourceRegistry } from '../data/source-registry.js'; +import { parseReplayCsv } from '../data/parse-replay-csv.js'; +import { PanelManager } from '../ui/panel-manager.js'; + +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + +function buildDeltaPoints(points) { + if (points.length < 2) { + return []; + } + + const derived = []; + for (let index = 1; index < points.length; index += 1) { + const previous = points[index - 1]; + const current = points[index]; + const deltaTime = Math.max(1, current.timeMs - previous.timeMs); + derived.push({ + ...current, + value: (current.value - previous.value) / deltaTime * 1000, + sourceId: `${current.sourceId}:delta`, + }); + } + + return derived; +} + +function buildSmoothedPoints(points, windowSize = 5) { + if (points.length === 0) { + return []; + } + + const smoothed = []; + for (let index = 0; index < points.length; index += 1) { + const start = Math.max(0, index - windowSize + 1); + const windowPoints = points.slice(start, index + 1); + const average = windowPoints.reduce((sum, point) => sum + point.value, 0) / windowPoints.length; + smoothed.push({ + ...points[index], + value: average, + sourceId: `${points[index].sourceId}:smooth`, + }); + } + + return smoothed; +} + +function transformPoints(points, transform) { + switch (transform) { + case 'delta': + return buildDeltaPoints(points); + case 'smooth': + return buildSmoothedPoints(points); + case 'raw': + default: + return points; + } +} + +function describeTransform(transform) { + switch (transform) { + case 'delta': + return 'Δvalue / second'; + case 'smooth': + return 'moving average'; + case 'raw': + default: + return 'raw signal'; + } +} + +function deriveValueRange(points, fallbackRange) { + if (points.length === 0) { + return fallbackRange; + } + + let min = Infinity; + let max = -Infinity; + for (const point of points) { + min = Math.min(min, point.value); + max = Math.max(max, point.value); + } + + const maxAbs = Math.max(Math.abs(min), Math.abs(max), 0.1); + return { + min: -maxAbs, + max: maxAbs, + }; +} + +function pickActiveHover(primaryCandidate, secondaryCandidate) { + if (!primaryCandidate && !secondaryCandidate) { + return null; + } + + if (primaryCandidate && !secondaryCandidate) { + return primaryCandidate; + } + + if (!primaryCandidate && secondaryCandidate) { + return secondaryCandidate; + } + + return primaryCandidate.lastPointerEventAt >= secondaryCandidate.lastPointerEventAt + ? primaryCandidate + : secondaryCandidate; +} + +export async function createApp(root) { + const bus = new EventBus(); + const store = new Store(createInitialState()); + const timeController = new TimeController(store); + const sourceBuffers = new Map(Object.keys(store.getState().sources).map((sourceKey) => [sourceKey, new PlotBuffer(store.getState().plot.maxPoints)])); + let sourceRegistry; + + const syncBuffersFromState = () => { + const state = store.getState(); + for (const sourceKey of Object.keys(state.sources)) { + if (!sourceBuffers.has(sourceKey)) { + sourceBuffers.set(sourceKey, new PlotBuffer(state.plot.maxPoints)); + } + sourceBuffers.get(sourceKey).maxPoints = state.plot.maxPoints; + } + + for (const sourceKey of Array.from(sourceBuffers.keys())) { + if (!state.sources[sourceKey]) { + sourceBuffers.delete(sourceKey); + } + } + }; + + const clearSourceBuffer = (sourceKey) => { + sourceBuffers.get(sourceKey)?.clear(); + }; + + const getGraphPoints = (state, graphId) => { + const graphConfig = state.graphs[graphId]; + const sourceBuffer = sourceBuffers.get(graphConfig.sourceKey); + const basePoints = sourceBuffer + ? sourceBuffer.getVisiblePoints(state.time.plotTimeMs, state.plot.windowDurationMs) + : []; + const transformedPoints = transformPoints(basePoints, graphConfig.transform); + return { + graphConfig, + points: transformedPoints, + range: deriveValueRange(transformedPoints, state.plot.valueRange), + }; + }; + + const actions = { + togglePause: () => timeController.togglePause(), + setSpeed: (speed) => timeController.setSpeed(speed), + resetScene: () => { + timeController.reset(); + sourceBuffers.forEach((plotBuffer) => plotBuffer.clear()); + sourceRegistry.reset(); + }, + togglePanel: (panelId) => { + store.setState((state) => ({ + ...state, + panels: { + ...state.panels, + [panelId]: { + ...state.panels[panelId], + visible: !state.panels[panelId].visible, + }, + }, + })); + }, + updateSource: (sourceKey, field, value) => { + store.setState((state) => ({ + ...state, + sources: { + ...state.sources, + [sourceKey]: { + ...state.sources[sourceKey], + [field]: value, + ...(field === 'type' + ? { + loadError: value === 'csv-replay' && state.sources[sourceKey].dataset.length === 0 + ? (state.sources[sourceKey].dataFileName + ? `Reload ${state.sources[sourceKey].dataFileName} to restore replay data` + : 'Load a CSV file to begin replay') + : '', + wsStatus: value === 'websocket' ? state.sources[sourceKey].wsStatus : 'idle', + wsStatusDetail: value === 'websocket' ? state.sources[sourceKey].wsStatusDetail : '', + } + : {}), + }, + }, + })); + sourceRegistry.syncFromState(); + syncBuffersFromState(); + + if (field === 'type' || field === 'wsUrl' || field === 'wsReconnectMs') { + clearSourceBuffer(sourceKey); + sourceRegistry.reset(); + } + }, + loadSourceFile: async (sourceKey, file) => { + try { + const state = store.getState(); + const sampleRateHz = state.sources[sourceKey]?.sampleRateHz ?? 60; + const text = await file.text(); + const { points, metadata } = parseReplayCsv(text, { sampleRateHz }); + + clearSourceBuffer(sourceKey); + store.setState((currentState) => ({ + ...currentState, + sources: { + ...currentState.sources, + [sourceKey]: { + ...currentState.sources[sourceKey], + type: 'csv-replay', + dataset: points, + dataFileName: file.name, + datasetPointCount: metadata.pointCount, + datasetDurationMs: metadata.durationMs, + loadError: '', + wsStatus: 'idle', + wsStatusDetail: '', + }, + }, + })); + sourceRegistry.syncFromState(); + sourceRegistry.reset(); + } catch (error) { + store.setState((currentState) => ({ + ...currentState, + sources: { + ...currentState.sources, + [sourceKey]: { + ...currentState.sources[sourceKey], + loadError: error instanceof Error ? error.message : String(error), + }, + }, + })); + } + }, + updatePlot: (field, value) => { + store.setState((state) => ({ + ...state, + plot: { + ...state.plot, + [field]: value, + }, + })); + + if (field === 'maxPoints') { + buffer.maxPoints = clamp(value, 200, 4000); + sourceBuffers.forEach((plotBuffer) => { + plotBuffer.maxPoints = clamp(value, 200, 4000); + }); + } + }, + updateGraph: (graphId, field, value) => { + store.setState((state) => ({ + ...state, + graphs: { + ...state.graphs, + [graphId]: { + ...state.graphs[graphId], + [field]: value, + }, + }, + })); + }, + }; + + const panelManager = new PanelManager({ root, store, actions }); + const elements = panelManager.mount(); + + const plotView = new TimeplotView({ + host: elements.primaryCanvasHost, + panelId: 'primary', + title: 'Primary signal', + subtitle: null, + showReadouts: true, + lineColor: 0x9fd1ff, + pointColor: 0xe7f2ff, + }); + + const secondaryPlotView = new TimeplotView({ + host: elements.secondaryCanvasHost, + panelId: 'secondary', + title: 'Secondary signal', + subtitle: null, + showReadouts: false, + lineColor: 0xffc46b, + pointColor: 0xffe1b0, + }); + + const renderer = await plotView.init(); + await secondaryPlotView.init(); + store.patch({ + app: { + ...store.getState().app, + renderer, + }, + }); + + sourceRegistry = new SourceRegistry(store, bus); + + bus.on('data:point', (point) => { + sourceBuffers.get(point.sourceId)?.addPoint(point); + }); + + const keyHandler = (event) => { + if (event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement) { + return; + } + + if (event.code === 'Space') { + event.preventDefault(); + actions.togglePause(); + return; + } + + if (event.key === '[') { + actions.setSpeed(store.getState().time.speed - 0.1); + return; + } + + if (event.key === ']') { + actions.setSpeed(store.getState().time.speed + 0.1); + return; + } + + if (event.key.toLowerCase() === 'g') { + actions.updatePlot('showGrid', !store.getState().plot.showGrid); + } + }; + + window.addEventListener('keydown', keyHandler); + + plotView.app.ticker.add(() => { + timeController.tick(); + sourceRegistry.syncFromState(); + syncBuffersFromState(); + sourceRegistry.update(store.getState().time.plotTimeMs); + + const state = store.getState(); + const primaryGraph = getGraphPoints(state, 'primary'); + const secondaryGraph = getGraphPoints(state, 'secondary'); + + plotView.panelTitle = state.graphs.primary.title; + plotView.panelSubtitle = `${state.sources[state.graphs.primary.sourceKey].label} · ${describeTransform(state.graphs.primary.transform)} · time ↓`; + secondaryPlotView.panelTitle = state.graphs.secondary.title; + secondaryPlotView.panelSubtitle = `${state.sources[state.graphs.secondary.sourceKey].label} · ${describeTransform(state.graphs.secondary.transform)} · time ↓`; + + const primaryState = { + ...state, + plot: { + ...state.plot, + valueRange: primaryGraph.range, + }, + }; + + const secondaryState = { + ...state, + plot: { + ...state.plot, + valueRange: secondaryGraph.range, + }, + }; + + plotView.render(primaryState, primaryGraph.points); + secondaryPlotView.render(secondaryState, secondaryGraph.points); + + const primaryHover = plotView.getHoverCandidate(); + const secondaryHover = secondaryPlotView.getHoverCandidate(); + const activeHover = pickActiveHover(primaryHover, secondaryHover); + + if (!activeHover) { + plotView.clearHover(); + secondaryPlotView.clearHover(); + store.setState((currentState) => ({ + ...currentState, + plot: { + ...currentState.plot, + hoveredPoint: null, + tooltip: { + ...currentState.plot.tooltip, + visible: false, + point: null, + linkedPoint: null, + }, + }, + })); + panelManager.sync(store.getState(), { + primary: primaryGraph.points.length, + secondary: secondaryGraph.points.length, + }); + return; + } + + const primaryLinkedPoint = plotView.findNearestScreenPointByTime(activeHover.point.timeMs); + const secondaryLinkedPoint = secondaryPlotView.findNearestScreenPointByTime(activeHover.point.timeMs); + + plotView.renderLinkedHover(primaryLinkedPoint); + secondaryPlotView.renderLinkedHover(secondaryLinkedPoint); + + const activePanelLabel = activeHover.panelId === 'secondary' + ? state.graphs.secondary.title + : state.graphs.primary.title; + const linkedPoint = activeHover.panelId === 'secondary' ? primaryLinkedPoint : secondaryLinkedPoint; + const linkedPanelLabel = activeHover.panelId === 'secondary' + ? state.graphs.primary.title + : state.graphs.secondary.title; + + store.setState((currentState) => ({ + ...currentState, + plot: { + ...currentState.plot, + hoveredPoint: activeHover.point, + tooltip: { + ...currentState.plot.tooltip, + visible: true, + panelId: activeHover.panelId, + panelLabel: activePanelLabel, + x: activeHover.x, + y: activeHover.y, + point: activeHover.point, + linkedPoint, + linkedPanelLabel, + }, + }, + })); + + panelManager.sync(store.getState(), { + primary: primaryGraph.points.length, + secondary: secondaryGraph.points.length, + }); + }); + + return { + destroy() { + window.removeEventListener('keydown', keyHandler); + plotView.destroy(); + secondaryPlotView.destroy(); + }, + }; +} diff --git a/src/bootstrap.js b/src/bootstrap.js new file mode 100644 index 0000000..4b073bc --- /dev/null +++ b/src/bootstrap.js @@ -0,0 +1,18 @@ +import './styles.css'; +import { createApp } from './app/create-app.js'; + +const root = document.getElementById('app'); + +if (!root) { + throw new Error('App root not found'); +} + +createApp(root).catch((error) => { + console.error('Failed to start TimePlot', error); + root.innerHTML = ` +
+

TimePlot failed to start

+
${String(error)}
+
+ `; +}); diff --git a/src/core/event-bus.js b/src/core/event-bus.js new file mode 100644 index 0000000..192eb6d --- /dev/null +++ b/src/core/event-bus.js @@ -0,0 +1,32 @@ +export class EventBus { + constructor() { + this.listeners = new Map(); + } + + on(eventName, listener) { + if (!this.listeners.has(eventName)) { + this.listeners.set(eventName, new Set()); + } + + const listeners = this.listeners.get(eventName); + listeners.add(listener); + + return () => { + listeners.delete(listener); + if (listeners.size === 0) { + this.listeners.delete(eventName); + } + }; + } + + emit(eventName, payload) { + const listeners = this.listeners.get(eventName); + if (!listeners) { + return; + } + + for (const listener of listeners) { + listener(payload); + } + } +} diff --git a/src/core/store.js b/src/core/store.js new file mode 100644 index 0000000..38052eb --- /dev/null +++ b/src/core/store.js @@ -0,0 +1,291 @@ +const STORAGE_KEY = 'timeplot.app-state.v1'; + +function clonePanelState(panels) { + return Object.fromEntries(Object.entries(panels).map(([key, value]) => [key, { ...value }])); +} + +function cloneNamedState(items) { + return Object.fromEntries(Object.entries(items).map(([key, value]) => [key, { ...value }])); +} + +function sanitizePersistedSource(source) { + return { + type: source.type, + preset: source.preset, + sampleRateHz: source.sampleRateHz, + amplitude: source.amplitude, + noise: source.noise, + replayRate: source.replayRate, + dataFileName: source.dataFileName, + wsUrl: source.wsUrl, + wsReconnectMs: source.wsReconnectMs, + }; +} + +function createPersistableState(state) { + return { + plot: { + showGrid: state.plot.showGrid, + showPoints: state.plot.showPoints, + windowDurationMs: state.plot.windowDurationMs, + maxPoints: state.plot.maxPoints, + }, + time: { + speed: state.time.speed, + }, + panels: clonePanelState(state.panels), + graphs: cloneNamedState(state.graphs), + sources: Object.fromEntries(Object.entries(state.sources).map(([key, value]) => [ + key, + sanitizePersistedSource(value), + ])), + }; +} + +function mergePersistedState(baseState, persistedState) { + if (!persistedState || typeof persistedState !== 'object') { + return baseState; + } + + const mergedState = { + ...baseState, + time: persistedState.time + ? { + ...baseState.time, + speed: persistedState.time.speed ?? baseState.time.speed, + paused: false, + } + : baseState.time, + plot: persistedState.plot + ? { + ...baseState.plot, + ...persistedState.plot, + valueRange: baseState.plot.valueRange, + hoveredPoint: null, + tooltip: { ...baseState.plot.tooltip }, + } + : baseState.plot, + panels: persistedState.panels + ? clonePanelState(Object.fromEntries(Object.entries(baseState.panels).map(([key, value]) => [ + key, + { + ...value, + ...(persistedState.panels[key] ?? {}), + }, + ]))) + : baseState.panels, + graphs: persistedState.graphs + ? cloneNamedState(Object.fromEntries(Object.entries(baseState.graphs).map(([key, value]) => [ + key, + { + ...value, + ...(persistedState.graphs[key] ?? {}), + }, + ]))) + : baseState.graphs, + sources: persistedState.sources + ? Object.fromEntries(Object.entries(baseState.sources).map(([key, value]) => { + const persistedSource = persistedState.sources[key] ?? {}; + const nextType = persistedSource.type ?? value.type; + + return [ + key, + { + ...value, + ...persistedSource, + type: nextType, + dataset: [], + datasetPointCount: 0, + datasetDurationMs: 0, + loadError: nextType === 'csv-replay' && persistedSource.dataFileName + ? `Reload ${persistedSource.dataFileName} to restore replay data` + : '', + wsStatus: 'idle', + wsStatusDetail: '', + }, + ]; + })) + : baseState.sources, + }; + + return mergedState; +} + +function loadPersistedState() { + if (typeof localStorage === 'undefined') { + return null; + } + + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) { + return null; + } + + return JSON.parse(raw); + } catch (error) { + console.warn('[timeplot] failed to load persisted state', error); + return null; + } +} + +function savePersistedState(state) { + if (typeof localStorage === 'undefined') { + return; + } + + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(createPersistableState(state))); + } catch (error) { + console.warn('[timeplot] failed to persist state', error); + } +} + +export function createInitialState() { + return { + app: { + title: 'TimePlot', + renderer: 'pending', + }, + time: { + realNowMs: Date.now(), + realElapsedMs: 0, + plotTimeMs: 0, + speed: 1, + paused: false, + }, + plot: { + showGrid: true, + showPoints: true, + windowDurationMs: 20000, + maxPoints: 1600, + valueRange: { + min: -1.6, + max: 1.6, + }, + hoveredPoint: null, + tooltip: { + visible: false, + x: 0, + y: 0, + point: null, + }, + }, + sources: { + signalA: { + id: 'signal-a', + label: 'Signal A', + type: 'synthetic-wave', + preset: 'telemetry', + sampleRateHz: 60, + amplitude: 1, + noise: 0.08, + replayRate: 1, + dataset: [], + dataFileName: '', + datasetPointCount: 0, + datasetDurationMs: 0, + loadError: '', + wsUrl: 'ws://localhost:8080', + wsReconnectMs: 2000, + wsStatus: 'idle', + wsStatusDetail: '', + }, + signalB: { + id: 'signal-b', + label: 'Signal B', + type: 'synthetic-wave', + preset: 'chirp', + sampleRateHz: 48, + amplitude: 0.8, + noise: 0.04, + replayRate: 1, + dataset: [], + dataFileName: '', + datasetPointCount: 0, + datasetDurationMs: 0, + loadError: '', + wsUrl: 'ws://localhost:8080', + wsReconnectMs: 2000, + wsStatus: 'idle', + wsStatusDetail: '', + }, + }, + graphs: { + primary: { + sourceKey: 'signalA', + transform: 'raw', + title: 'Primary signal', + }, + secondary: { + sourceKey: 'signalB', + transform: 'delta', + title: 'Secondary signal', + }, + }, + panels: { + status: { title: 'Status', visible: true }, + source: { title: 'Data Source', visible: true }, + config: { title: 'Config', visible: true }, + help: { title: 'Help', visible: false }, + }, + }; +} + +export class Store { + constructor(initialState = createInitialState()) { + this.state = mergePersistedState(initialState, loadPersistedState()); + this.listeners = new Set(); + } + + getState() { + return this.state; + } + + subscribe(listener) { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + setState(updater) { + const nextState = typeof updater === 'function' ? updater(this.state) : updater; + this.state = nextState; + savePersistedState(this.state); + for (const listener of this.listeners) { + listener(this.state); + } + } + + patch(partial) { + this.setState((state) => ({ + ...state, + ...partial, + time: partial.time ? { ...state.time, ...partial.time } : state.time, + plot: partial.plot + ? { + ...state.plot, + ...partial.plot, + valueRange: partial.plot.valueRange + ? { ...state.plot.valueRange, ...partial.plot.valueRange } + : state.plot.valueRange, + tooltip: partial.plot.tooltip + ? { ...state.plot.tooltip, ...partial.plot.tooltip } + : state.plot.tooltip, + } + : state.plot, + sources: partial.sources + ? Object.fromEntries(Object.entries({ ...state.sources, ...partial.sources }).map(([key, value]) => [ + key, + { ...state.sources[key], ...value }, + ])) + : state.sources, + graphs: partial.graphs + ? cloneNamedState(Object.fromEntries(Object.entries({ ...state.graphs, ...partial.graphs }).map(([key, value]) => [ + key, + { ...state.graphs[key], ...value }, + ]))) + : state.graphs, + panels: partial.panels ? clonePanelState({ ...state.panels, ...partial.panels }) : state.panels, + })); + } +} diff --git a/src/core/time-controller.js b/src/core/time-controller.js new file mode 100644 index 0000000..7cd57c7 --- /dev/null +++ b/src/core/time-controller.js @@ -0,0 +1,80 @@ +export class TimeController { + constructor(store) { + this.store = store; + this.lastFrameTime = performance.now(); + } + + tick(now = performance.now()) { + const deltaMs = now - this.lastFrameTime; + this.lastFrameTime = now; + + this.store.setState((state) => { + const realElapsedMs = state.time.realElapsedMs + deltaMs; + const plotDeltaMs = state.time.paused ? 0 : deltaMs * state.time.speed; + + return { + ...state, + time: { + ...state.time, + realNowMs: Date.now(), + realElapsedMs, + plotTimeMs: Math.max(0, state.time.plotTimeMs + plotDeltaMs), + }, + }; + }); + + return deltaMs; + } + + togglePause() { + this.store.setState((state) => ({ + ...state, + time: { + ...state.time, + paused: !state.time.paused, + }, + })); + } + + setPaused(paused) { + this.store.setState((state) => ({ + ...state, + time: { + ...state.time, + paused, + }, + })); + } + + setSpeed(speed) { + const clampedSpeed = Math.max(0.1, Math.min(12, speed)); + this.store.setState((state) => ({ + ...state, + time: { + ...state.time, + speed: clampedSpeed, + }, + })); + } + + reset() { + this.store.setState((state) => ({ + ...state, + time: { + ...state.time, + realElapsedMs: 0, + plotTimeMs: 0, + }, + plot: { + ...state.plot, + hoveredPoint: null, + tooltip: { + ...state.plot.tooltip, + visible: false, + point: null, + }, + }, + })); + this.lastFrameTime = performance.now(); + } +} diff --git a/src/data-sources.js b/src/data-sources.js new file mode 100644 index 0000000..749a151 --- /dev/null +++ b/src/data-sources.js @@ -0,0 +1,517 @@ +/** + * Data Sources - Components that generate or provide data to plots + * + * This module implements the data provider side of the architecture. + * Data sources know how to generate or fetch data, but don't know + * anything about visualization. + * + * Architecture: + * - DataSource: Base class with event emitting + * - Specific sources: Implement different data generation strategies + * - Connection: Links sources to plots (see plot-connections.js) + */ + +// Simple EventEmitter (same as in state.js, could be extracted to utils) +class EventEmitter { + constructor() { + this.events = new Map(); + } + + on(event, callback) { + if (!this.events.has(event)) { + this.events.set(event, []); + } + this.events.get(event).push(callback); + return () => this.off(event, callback); + } + + off(event, callback) { + if (!this.events.has(event)) return; + const callbacks = this.events.get(event); + const index = callbacks.indexOf(callback); + if (index > -1) { + callbacks.splice(index, 1); + } + } + + emit(event, data) { + if (!this.events.has(event)) return; + this.events.get(event).forEach(callback => { + try { + callback(data); + } catch (e) { + console.error(`[DataSource] Error in event handler for '${event}':`, e); + } + }); + } +} + +/** + * Base class for all data sources + * + * Events emitted: + * - 'line': {points: Array, timestamp: number, metadata: Object} + * - 'point': {value: number, timestamp: number} + * - 'error': {error: Error} + */ +export class DataSource extends EventEmitter { + constructor(config = {}) { + super(); + this.config = config; + this.isRunning = false; + this.time = 0; + } + + /** + * Start generating/providing data + */ + start() { + this.isRunning = true; + } + + /** + * Stop generating/providing data + */ + stop() { + this.isRunning = false; + } + + /** + * Reset the data source to initial state + */ + reset() { + this.time = 0; + } + + /** + * Emit a complete line of data + */ + emitLine(points, metadata = {}) { + this.emit('line', { + points, + timestamp: metadata.timestamp || Date.now(), + metadata, + }); + } + + /** + * Emit a single data point + */ + emitPoint(value, timestamp = Date.now()) { + this.emit('point', { + value, + timestamp, + }); + } + + /** + * Emit an error + */ + emitError(error) { + this.emit('error', { error }); + } +} + +/** + * Synthetic data source using test generators + * Uses the generators from test-data-generators.js + */ +export class SyntheticDataSource extends DataSource { + constructor(config = {}) { + super(config); + this.generator = config.generator; // Instance of DataGenerator + this.pointsPerLine = config.pointsPerLine || 100; + this.width = config.width || 800; + this.lineInterval = config.lineInterval || 100; // ms between lines + this.intervalHandle = null; + } + + start() { + if (this.isRunning) return; + super.start(); + + // Generate a new line periodically + this.intervalHandle = setInterval(() => { + this.generateAndEmitLine(); + }, this.lineInterval); + + // Generate initial line immediately + this.generateAndEmitLine(); + } + + stop() { + super.stop(); + if (this.intervalHandle) { + clearInterval(this.intervalHandle); + this.intervalHandle = null; + } + } + + generateAndEmitLine() { + if (!this.generator) { + this.emitError(new Error('No generator configured')); + return; + } + + const points = this.generator.generateLine(this.pointsPerLine, this.width); + this.emitLine(points, { + timestamp: Date.now(), + generatorType: this.generator.constructor.name, + }); + } + + setGenerator(generator) { + this.generator = generator; + } +} + +/** + * Function-based data source + * Evaluates a user-provided function to generate data + */ +export class FunctionDataSource extends DataSource { + constructor(config = {}) { + super(config); + // Function should have signature: (x, t) => y + // x: normalized position 0-1 + // t: time in seconds + // returns: y value + this.func = config.func || ((x, t) => Math.sin(x * 10 + t)); + this.pointsPerLine = config.pointsPerLine || 100; + this.width = config.width || 800; + this.amplitude = config.amplitude || 30; + this.lineInterval = config.lineInterval || 100; + this.intervalHandle = null; + } + + start() { + if (this.isRunning) return; + super.start(); + + this.intervalHandle = setInterval(() => { + this.generateAndEmitLine(); + }, this.lineInterval); + + this.generateAndEmitLine(); + } + + stop() { + super.stop(); + if (this.intervalHandle) { + clearInterval(this.intervalHandle); + this.intervalHandle = null; + } + } + + generateAndEmitLine() { + const points = []; + const t = this.time; + + for (let i = 0; i < this.pointsPerLine; i++) { + const x = (i / this.pointsPerLine) * this.width; + const normalizedX = i / this.pointsPerLine; + const y = this.func(normalizedX, t) * this.amplitude; + points.push({ x, y }); + } + + this.emitLine(points, { + timestamp: Date.now(), + time: t, + }); + + this.time += this.lineInterval / 1000; + } + + setFunction(func) { + this.func = func; + } +} + +/** + * Streaming data source + * Emits individual data points that get buffered into lines + */ +export class StreamingDataSource extends DataSource { + constructor(config = {}) { + super(config); + this.generator = config.generator; + this.sampleRate = config.sampleRate || 60; // Samples per second + this.intervalHandle = null; + } + + start() { + if (this.isRunning) return; + super.start(); + + const intervalMs = 1000 / this.sampleRate; + this.intervalHandle = setInterval(() => { + this.generateAndEmitPoint(); + }, intervalMs); + } + + stop() { + super.stop(); + if (this.intervalHandle) { + clearInterval(this.intervalHandle); + this.intervalHandle = null; + } + } + + generateAndEmitPoint() { + if (!this.generator) { + this.emitError(new Error('No generator configured')); + return; + } + + const value = this.generator.sample(); + this.generator.time += 1 / this.generator.sampleRate; + this.emitPoint(value, Date.now()); + } + + setGenerator(generator) { + this.generator = generator; + } +} + +/** + * WebSocket data source (for real data) + * Receives data from a WebSocket connection + */ +export class WebSocketDataSource extends DataSource { + constructor(config = {}) { + super(config); + this.url = config.url; + this.socket = null; + this.reconnectInterval = config.reconnectInterval || 5000; + this.reconnectHandle = null; + } + + start() { + if (this.isRunning) return; + super.start(); + this.connect(); + } + + stop() { + super.stop(); + if (this.socket) { + this.socket.close(); + this.socket = null; + } + if (this.reconnectHandle) { + clearTimeout(this.reconnectHandle); + this.reconnectHandle = null; + } + } + + connect() { + try { + this.socket = new WebSocket(this.url); + + this.socket.onopen = () => { + console.log(`[WebSocketDataSource] Connected to ${this.url}`); + }; + + this.socket.onmessage = (event) => { + this.handleMessage(event.data); + }; + + this.socket.onerror = (error) => { + console.error('[WebSocketDataSource] Error:', error); + this.emitError(error); + }; + + this.socket.onclose = () => { + console.log('[WebSocketDataSource] Connection closed'); + if (this.isRunning) { + // Auto-reconnect + this.reconnectHandle = setTimeout(() => { + this.connect(); + }, this.reconnectInterval); + } + }; + } catch (error) { + console.error('[WebSocketDataSource] Failed to connect:', error); + this.emitError(error); + } + } + + handleMessage(data) { + try { + const parsed = JSON.parse(data); + + // Expect format: {type: 'line', points: [...]} or {type: 'point', value: ...} + if (parsed.type === 'line' && parsed.points) { + this.emitLine(parsed.points, parsed.metadata || {}); + } else if (parsed.type === 'point' && parsed.value !== undefined) { + this.emitPoint(parsed.value, parsed.timestamp); + } else { + console.warn('[WebSocketDataSource] Unknown message format:', parsed); + } + } catch (error) { + console.error('[WebSocketDataSource] Failed to parse message:', error); + this.emitError(error); + } + } + + send(data) { + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + this.socket.send(JSON.stringify(data)); + } + } +} + +/** + * CSV File data source + * Reads data from CSV files (for replay/analysis) + */ +export class CSVDataSource extends DataSource { + constructor(config = {}) { + super(config); + this.data = []; // Parsed CSV data + this.currentIndex = 0; + this.playbackRate = config.playbackRate || 1.0; + this.loop = config.loop || false; + this.intervalHandle = null; + } + + /** + * Load CSV data from a string + * Expected format: timestamp,value or x,y format + */ + loadCSV(csvString) { + const lines = csvString.trim().split('\n'); + const headers = lines[0].split(',').map(h => h.trim()); + + this.data = []; + for (let i = 1; i < lines.length; i++) { + const values = lines[i].split(',').map(v => parseFloat(v.trim())); + if (values.length >= 2 && !values.some(isNaN)) { + this.data.push({ + timestamp: values[0], + value: values[1], + }); + } + } + + console.log(`[CSVDataSource] Loaded ${this.data.length} data points`); + } + + start() { + if (this.isRunning || this.data.length === 0) return; + super.start(); + + // Play back at specified rate + this.intervalHandle = setInterval(() => { + this.emitNextPoint(); + }, 16 / this.playbackRate); // ~60fps adjusted by playback rate + } + + stop() { + super.stop(); + if (this.intervalHandle) { + clearInterval(this.intervalHandle); + this.intervalHandle = null; + } + } + + reset() { + super.reset(); + this.currentIndex = 0; + } + + emitNextPoint() { + if (this.currentIndex >= this.data.length) { + if (this.loop) { + this.currentIndex = 0; + } else { + this.stop(); + return; + } + } + + const point = this.data[this.currentIndex]; + this.emitPoint(point.value, point.timestamp); + this.currentIndex++; + } +} + +/** + * Multi-source combiner + * Combines data from multiple sources + */ +export class CompositeDataSource extends DataSource { + constructor(config = {}) { + super(config); + this.sources = config.sources || []; + this.combineMode = config.combineMode || 'average'; // 'average', 'sum', 'max', 'min' + this.pointBuffer = new Map(); // sourceId => latest point + } + + start() { + if (this.isRunning) return; + super.start(); + + // Subscribe to all sources + this.sources.forEach((source, idx) => { + source.on('point', (data) => { + this.handleSourcePoint(idx, data); + }); + source.on('line', (data) => { + this.handleSourceLine(idx, data); + }); + source.start(); + }); + } + + stop() { + super.stop(); + this.sources.forEach(source => source.stop()); + } + + handleSourcePoint(sourceIdx, data) { + this.pointBuffer.set(sourceIdx, data.value); + + // If we have data from all sources, combine and emit + if (this.pointBuffer.size === this.sources.length) { + const combined = this.combineValues(Array.from(this.pointBuffer.values())); + this.emitPoint(combined, data.timestamp); + } + } + + handleSourceLine(sourceIdx, data) { + // For lines, just pass through for now + // Could implement line combination if needed + this.emitLine(data.points, data.metadata); + } + + combineValues(values) { + switch (this.combineMode) { + case 'sum': + return values.reduce((a, b) => a + b, 0); + case 'average': + return values.reduce((a, b) => a + b, 0) / values.length; + case 'max': + return Math.max(...values); + case 'min': + return Math.min(...values); + default: + return values[0]; + } + } + + addSource(source) { + this.sources.push(source); + if (this.isRunning) { + source.start(); + } + } + + removeSource(source) { + const idx = this.sources.indexOf(source); + if (idx > -1) { + source.stop(); + this.sources.splice(idx, 1); + } + } +} diff --git a/src/data/base-source.js b/src/data/base-source.js new file mode 100644 index 0000000..55dbdc3 --- /dev/null +++ b/src/data/base-source.js @@ -0,0 +1,21 @@ +export class BaseSource { + constructor(config = {}) { + this.config = { ...config }; + this.running = false; + } + + start() { + this.running = true; + } + + stop() { + this.running = false; + } + + updateConfig(nextConfig) { + this.config = { + ...this.config, + ...nextConfig, + }; + } +} diff --git a/src/data/csv-replay-source.js b/src/data/csv-replay-source.js new file mode 100644 index 0000000..c4e6a66 --- /dev/null +++ b/src/data/csv-replay-source.js @@ -0,0 +1,60 @@ +import { BaseSource } from './base-source.js'; + +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + +export class CsvReplaySource extends BaseSource { + constructor(config = {}) { + super({ + replayRate: 1, + dataset: [], + ...config, + }); + this.sourceType = 'csv-replay'; + this.nextPointIndex = 0; + } + + start(startTimeMs = 0) { + super.start(); + this.reset(startTimeMs); + } + + reset() { + this.nextPointIndex = 0; + } + + updateConfig(nextConfig) { + const datasetChanged = nextConfig.dataset !== this.config.dataset; + super.updateConfig(nextConfig); + if (datasetChanged) { + this.reset(); + } + } + + update(currentPlotTimeMs) { + if (!this.running || !Array.isArray(this.config.dataset) || this.config.dataset.length === 0) { + return []; + } + + const replayRate = clamp(this.config.replayRate ?? 1, 0.1, 8); + const targetDatasetTimeMs = currentPlotTimeMs * replayRate; + const points = []; + + while (this.nextPointIndex < this.config.dataset.length) { + const datasetPoint = this.config.dataset[this.nextPointIndex]; + if (datasetPoint.timeMs > targetDatasetTimeMs) { + break; + } + + points.push({ + timeMs: datasetPoint.timeMs / replayRate, + value: datasetPoint.value, + sourceId: this.config.id ?? 'csv-replay', + }); + this.nextPointIndex += 1; + } + + return points; + } +} diff --git a/src/data/parse-replay-csv.js b/src/data/parse-replay-csv.js new file mode 100644 index 0000000..b6ce97a --- /dev/null +++ b/src/data/parse-replay-csv.js @@ -0,0 +1,108 @@ +function splitRow(line) { + return line.split(/[;,\t]/).map((value) => value.trim()); +} + +function isNumeric(value) { + return value !== '' && Number.isFinite(Number(value)); +} + +function detectHeader(rows) { + if (rows.length === 0) { + return { hasHeader: false, headers: [] }; + } + + const [firstRow] = rows; + const hasHeader = firstRow.some((value) => !isNumeric(value)); + return { + hasHeader, + headers: hasHeader ? firstRow.map((value) => value.toLowerCase()) : [], + }; +} + +function detectTimeScale(headers) { + const timeHeader = headers.find((header) => header.includes('time') || header.includes('timestamp')); + if (!timeHeader) { + return 1; + } + + if (timeHeader.includes('sec') && !timeHeader.includes('msec') && !timeHeader.includes('ms')) { + return 1000; + } + + return 1; +} + +function detectColumnIndexes(headers, columnCount) { + if (headers.length === 0) { + return { + timeIndex: columnCount > 1 ? 0 : -1, + valueIndex: columnCount > 1 ? 1 : 0, + }; + } + + const timeIndex = headers.findIndex((header) => header.includes('time') || header.includes('timestamp')); + const valueIndex = headers.findIndex((header) => header.includes('value') || header.includes('signal') || header.includes('y')); + + return { + timeIndex, + valueIndex: valueIndex >= 0 ? valueIndex : (headers.length > 1 ? 1 : 0), + }; +} + +export function parseReplayCsv(text, { sampleRateHz = 60 } = {}) { + const rows = text + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')) + .map(splitRow) + .filter((row) => row.some((value) => value !== '')); + + if (rows.length === 0) { + throw new Error('CSV file is empty'); + } + + const { hasHeader, headers } = detectHeader(rows); + const dataRows = hasHeader ? rows.slice(1) : rows; + const columnCount = rows[0].length; + const { timeIndex, valueIndex } = detectColumnIndexes(headers, columnCount); + const timeScale = detectTimeScale(headers); + const intervalMs = 1000 / Math.max(1, sampleRateHz); + + const points = dataRows + .map((row, index) => { + const rawValue = row[valueIndex]; + if (!isNumeric(rawValue)) { + return null; + } + + const parsedValue = Number(rawValue); + const parsedTime = timeIndex >= 0 && isNumeric(row[timeIndex]) + ? Number(row[timeIndex]) * timeScale + : index * intervalMs; + + return { + timeMs: parsedTime, + value: parsedValue, + }; + }) + .filter(Boolean) + .sort((left, right) => left.timeMs - right.timeMs); + + if (points.length === 0) { + throw new Error('CSV file did not contain any numeric data points'); + } + + const firstTime = points[0].timeMs; + const normalizedPoints = points.map((point) => ({ + timeMs: point.timeMs - firstTime, + value: point.value, + })); + + return { + points: normalizedPoints, + metadata: { + pointCount: normalizedPoints.length, + durationMs: normalizedPoints.at(-1)?.timeMs ?? 0, + }, + }; +} diff --git a/src/data/source-registry.js b/src/data/source-registry.js new file mode 100644 index 0000000..917d06b --- /dev/null +++ b/src/data/source-registry.js @@ -0,0 +1,90 @@ +import { CsvReplaySource } from './csv-replay-source.js'; +import { SyntheticWaveSource } from './synthetic-wave-source.js'; +import { WebSocketSource } from './websocket-source.js'; + +export class SourceRegistry { + constructor(store, bus) { + this.store = store; + this.bus = bus; + this.sources = new Map(); + this.syncFromState(); + } + + syncFromState() { + const state = this.store.getState(); + const sourceEntries = Object.entries(state.sources); + const activeKeys = new Set(sourceEntries.map(([sourceKey]) => sourceKey)); + + for (const [sourceKey, config] of sourceEntries) { + const existingSource = this.sources.get(sourceKey); + + if (!existingSource) { + const nextSource = this.createSource(sourceKey, config); + this.sources.set(sourceKey, nextSource); + nextSource.start(state.time.plotTimeMs); + continue; + } + + if (existingSource.sourceType !== config.type) { + existingSource.stop(); + const replacementSource = this.createSource(sourceKey, config); + this.sources.set(sourceKey, replacementSource); + replacementSource.start(state.time.plotTimeMs); + continue; + } + + existingSource.updateConfig(config); + } + + for (const [sourceKey, source] of this.sources.entries()) { + if (!activeKeys.has(sourceKey)) { + source.stop(); + this.sources.delete(sourceKey); + } + } + } + + createSource(sourceKey, config) { + switch (config.type) { + case 'csv-replay': + return new CsvReplaySource(config); + case 'websocket': + return new WebSocketSource(config, { + onStatusChange: (statusPatch) => { + this.store.setState((state) => ({ + ...state, + sources: { + ...state.sources, + [sourceKey]: { + ...state.sources[sourceKey], + ...statusPatch, + }, + }, + })); + }, + }); + case 'synthetic-wave': + default: + return new SyntheticWaveSource(config); + } + } + + update(currentPlotTimeMs) { + for (const [sourceKey, source] of this.sources.entries()) { + const points = source.update(currentPlotTimeMs); + for (const point of points) { + this.bus.emit('data:point', { + ...point, + sourceId: sourceKey, + }); + } + } + } + + reset() { + const startTimeMs = this.store.getState().time.plotTimeMs; + for (const source of this.sources.values()) { + source.reset(startTimeMs); + } + } +} diff --git a/src/data/synthetic-wave-source.js b/src/data/synthetic-wave-source.js new file mode 100644 index 0000000..df53319 --- /dev/null +++ b/src/data/synthetic-wave-source.js @@ -0,0 +1,87 @@ +import { BaseSource } from './base-source.js'; + +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + +function createDeterministicNoise(seed) { + const x = Math.sin(seed * 12.9898) * 43758.5453; + return x - Math.floor(x); +} + +export class SyntheticWaveSource extends BaseSource { + constructor(config = {}) { + super({ + sampleRateHz: 60, + preset: 'telemetry', + amplitude: 1, + noise: 0.08, + ...config, + }); + this.sourceType = 'synthetic-wave'; + this.lastEmittedPlotTimeMs = 0; + } + + start(startTimeMs = 0) { + super.start(); + this.lastEmittedPlotTimeMs = startTimeMs; + } + + stop() { + super.stop(); + } + + reset(startTimeMs = 0) { + this.lastEmittedPlotTimeMs = startTimeMs; + } + + sampleValue(timeMs) { + const seconds = timeMs / 1000; + const amplitude = this.config.amplitude; + const noise = this.config.noise; + const grain = (createDeterministicNoise(timeMs * 0.017) - 0.5) * 2 * noise; + + switch (this.config.preset) { + case 'chirp': { + const sweep = Math.sin(seconds * seconds * 1.4); + return amplitude * (0.7 * sweep + 0.3 * Math.sin(seconds * 7.5)) + grain; + } + case 'burst': { + const burstPhase = (seconds % 6) - 1.5; + const burst = Math.sin(seconds * 9.5) * Math.exp(-(burstPhase ** 2) * 0.8); + return amplitude * (0.45 * Math.sin(seconds * 2.1) + burst) + grain; + } + case 'telemetry': + default: { + const carrier = Math.sin(seconds * 2.2); + const secondary = 0.35 * Math.cos(seconds * 6.4 + Math.sin(seconds * 0.8)); + const envelope = 0.15 * Math.sin(seconds * 0.33); + return amplitude * (carrier + secondary + envelope) + grain; + } + } + } + + update(currentPlotTimeMs) { + if (!this.running) { + return []; + } + + const intervalMs = 1000 / clamp(this.config.sampleRateHz, 1, 240); + if (currentPlotTimeMs < this.lastEmittedPlotTimeMs) { + this.lastEmittedPlotTimeMs = currentPlotTimeMs; + return []; + } + + const points = []; + while (this.lastEmittedPlotTimeMs + intervalMs <= currentPlotTimeMs) { + this.lastEmittedPlotTimeMs += intervalMs; + points.push({ + timeMs: this.lastEmittedPlotTimeMs, + value: this.sampleValue(this.lastEmittedPlotTimeMs), + sourceId: 'synthetic-wave', + }); + } + + return points; + } +} diff --git a/src/data/websocket-source.js b/src/data/websocket-source.js new file mode 100644 index 0000000..5458fb9 --- /dev/null +++ b/src/data/websocket-source.js @@ -0,0 +1,224 @@ +import { BaseSource } from './base-source.js'; + +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + +function isFiniteNumber(value) { + return typeof value === 'number' && Number.isFinite(value); +} + +function parsePayload(payload) { + if (Array.isArray(payload)) { + return payload.flatMap((item) => parsePayload(item)); + } + + if (isFiniteNumber(payload)) { + return [{ value: payload, timestampMs: null }]; + } + + if (typeof payload === 'string') { + const trimmed = payload.trim(); + if (!trimmed) { + return []; + } + + const numeric = Number(trimmed); + if (Number.isFinite(numeric)) { + return [{ value: numeric, timestampMs: null }]; + } + + try { + return parsePayload(JSON.parse(trimmed)); + } catch { + return []; + } + } + + if (payload && typeof payload === 'object') { + const candidateValue = [payload.value, payload.y, payload.signal, payload.data] + .find((value) => Number.isFinite(Number(value))); + + if (candidateValue === undefined) { + return []; + } + + const candidateTimestamp = [payload.timeMs, payload.timestampMs, payload.timestamp, payload.t] + .find((value) => Number.isFinite(Number(value))); + + return [{ + value: Number(candidateValue), + timestampMs: candidateTimestamp === undefined ? null : Number(candidateTimestamp), + }]; + } + + return []; +} + +export class WebSocketSource extends BaseSource { + constructor(config = {}, { onStatusChange } = {}) { + super({ + wsUrl: 'ws://localhost:8080', + wsReconnectMs: 2000, + ...config, + }); + this.sourceType = 'websocket'; + this.onStatusChange = onStatusChange; + this.socket = null; + this.queue = []; + this.lastPlotTimeMs = 0; + this.reconnectTimer = null; + this.shouldReconnect = false; + this.firstSourceTimestampMs = null; + this.basePlotTimeMs = 0; + } + + start(startTimeMs = 0) { + super.start(); + this.lastPlotTimeMs = startTimeMs; + this.basePlotTimeMs = startTimeMs; + this.shouldReconnect = true; + this.connect(); + } + + stop() { + super.stop(); + this.shouldReconnect = false; + this.clearReconnectTimer(); + if (this.socket) { + this.socket.close(); + this.socket = null; + } + this.setStatus('disconnected', 'socket closed'); + } + + reset(startTimeMs = 0) { + this.queue = []; + this.lastPlotTimeMs = startTimeMs; + this.basePlotTimeMs = startTimeMs; + this.firstSourceTimestampMs = null; + } + + updateConfig(nextConfig) { + const previousUrl = this.config.wsUrl; + const previousReconnectMs = this.config.wsReconnectMs; + super.updateConfig(nextConfig); + + if ((previousUrl !== this.config.wsUrl || previousReconnectMs !== this.config.wsReconnectMs) && this.running) { + this.reconnect(); + } + } + + update(currentPlotTimeMs) { + this.lastPlotTimeMs = currentPlotTimeMs; + + if (this.queue.length === 0) { + return []; + } + + const points = []; + while (this.queue.length > 0) { + const nextPoint = this.queue.shift(); + let timeMs = currentPlotTimeMs; + + if (isFiniteNumber(nextPoint.timestampMs)) { + if (this.firstSourceTimestampMs === null) { + this.firstSourceTimestampMs = nextPoint.timestampMs; + this.basePlotTimeMs = currentPlotTimeMs; + } + timeMs = this.basePlotTimeMs + (nextPoint.timestampMs - this.firstSourceTimestampMs); + } + + points.push({ + timeMs, + value: nextPoint.value, + sourceId: this.config.id ?? 'websocket', + }); + } + + return points; + } + + reconnect() { + if (!this.running) { + return; + } + + this.clearReconnectTimer(); + if (this.socket) { + this.socket.close(); + this.socket = null; + } + this.connect(); + } + + connect() { + const url = this.config.wsUrl?.trim(); + if (!url) { + this.setStatus('idle', 'enter a websocket url'); + return; + } + + this.clearReconnectTimer(); + this.setStatus('connecting', url); + + try { + this.socket = new WebSocket(url); + } catch (error) { + this.setStatus('error', error instanceof Error ? error.message : String(error)); + this.scheduleReconnect(); + return; + } + + this.socket.addEventListener('open', () => { + this.setStatus('connected', url); + }); + + this.socket.addEventListener('message', (event) => { + const parsedPoints = parsePayload(event.data); + if (parsedPoints.length === 0) { + return; + } + this.queue.push(...parsedPoints); + }); + + this.socket.addEventListener('error', () => { + this.setStatus('error', 'socket error'); + }); + + this.socket.addEventListener('close', () => { + this.socket = null; + if (!this.running) { + return; + } + this.setStatus('disconnected', 'retrying'); + this.scheduleReconnect(); + }); + } + + scheduleReconnect() { + if (!this.shouldReconnect || !this.running) { + return; + } + + const reconnectMs = clamp(Number(this.config.wsReconnectMs) || 2000, 250, 30000); + this.clearReconnectTimer(); + this.reconnectTimer = window.setTimeout(() => { + this.connect(); + }, reconnectMs); + } + + clearReconnectTimer() { + if (this.reconnectTimer !== null) { + window.clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + } + + setStatus(status, detail = '') { + this.onStatusChange?.({ + wsStatus: status, + wsStatusDetail: detail, + }); + } +} diff --git a/src/demos.js b/src/demos.js new file mode 100644 index 0000000..1dd6785 --- /dev/null +++ b/src/demos.js @@ -0,0 +1,697 @@ +/** + * Preloaded Graphics Demos + * + * Each demo exports: + * - name: Display name + * - description: Short description + * - setup(app, state): Called once to create objects + * - update(app, state, objects): Called every frame + * - cleanup(app, objects): Called when switching demos + */ + +// ============================================================================ +// DEMO 1: BOUNCING PARTICLES +// ============================================================================ + +export const bouncingParticles = { + name: "Bouncing Particles", + description: "Colorful particles bouncing around the screen", + + setup(app, state) { + const particles = []; + const colors = [0xff6b6b, 0x4ecdc4, 0x45b7d1, 0xf9ca24, 0x6c5ce7]; + + for (let i = 0; i < 50; i++) { + const particle = new PIXI.Graphics(); + const size = 5 + Math.random() * 10; + particle.circle(0, 0, size); + particle.fill(colors[Math.floor(Math.random() * colors.length)]); + + particle.x = Math.random() * app.screen.width; + particle.y = Math.random() * app.screen.height; + particle.vx = (Math.random() - 0.5) * 8; + particle.vy = (Math.random() - 0.5) * 8; + particle.size = size; + + app.stage.addChild(particle); + particles.push(particle); + } + + return { particles }; + }, + + update(app, state, objects) { + objects.particles.forEach(p => { + p.x += p.vx; + p.y += p.vy; + + // Bounce off edges + if (p.x < p.size || p.x > app.screen.width - p.size) p.vx *= -1; + if (p.y < p.size || p.y > app.screen.height - p.size) p.vy *= -1; + + // Clamp to screen + p.x = Math.max(p.size, Math.min(app.screen.width - p.size, p.x)); + p.y = Math.max(p.size, Math.min(app.screen.height - p.size, p.y)); + }); + }, + + cleanup(app, objects) { + objects.particles.forEach(p => p.destroy()); + } +}; + +// ============================================================================ +// DEMO 2: SPIROGRAPH +// ============================================================================ + +export const spirograph = { + name: "Spirograph", + description: "Mesmerizing geometric spiral patterns", + + setup(app, state) { + const graphics = new PIXI.Graphics(); + app.stage.addChild(graphics); + + return { + graphics, + angle: 0, + points: [] + }; + }, + + update(app, state, objects) { + const cx = app.screen.width / 2; + const cy = app.screen.height / 2; + const t = state.state.time.current; + + // Generate new point + const r1 = 150; + const r2 = 50; + const r3 = 30; + + const x = cx + Math.cos(t * 0.5) * r1 + Math.cos(t * 2) * r2 + Math.cos(t * 5) * r3; + const y = cy + Math.sin(t * 0.5) * r1 + Math.sin(t * 2) * r2 + Math.sin(t * 5) * r3; + + objects.points.push({ x, y }); + + // Keep only last 500 points + if (objects.points.length > 500) { + objects.points.shift(); + } + + // Draw trail + objects.graphics.clear(); + if (objects.points.length > 1) { + for (let i = 1; i < objects.points.length; i++) { + const alpha = i / objects.points.length; + const hue = (i / objects.points.length) * 360; + objects.graphics.moveTo(objects.points[i-1].x, objects.points[i-1].y); + objects.graphics.lineTo(objects.points[i].x, objects.points[i].y); + objects.graphics.stroke({ width: 2, color: hslToHex(hue, 100, 60), alpha }); + } + } + }, + + cleanup(app, objects) { + objects.graphics.destroy(); + } +}; + +// ============================================================================ +// DEMO 3: STARFIELD +// ============================================================================ + +export const starfield = { + name: "Starfield", + description: "Flying through space at warp speed", + + setup(app, state) { + const stars = []; + + for (let i = 0; i < 200; i++) { + const star = new PIXI.Graphics(); + star.circle(0, 0, 2); + star.fill(0xffffff); + + star.x = (Math.random() - 0.5) * app.screen.width * 2; + star.y = (Math.random() - 0.5) * app.screen.height * 2; + star.z = Math.random() * 1000; + + app.stage.addChild(star); + stars.push(star); + } + + return { stars }; + }, + + update(app, state, objects) { + const cx = app.screen.width / 2; + const cy = app.screen.height / 2; + const speed = 5; + + objects.stars.forEach(star => { + star.z -= speed; + + if (star.z <= 0) { + star.z = 1000; + star.x = (Math.random() - 0.5) * app.screen.width * 2; + star.y = (Math.random() - 0.5) * app.screen.height * 2; + } + + const screenX = cx + (star.x / star.z) * 200; + const screenY = cy + (star.y / star.z) * 200; + const size = (1 - star.z / 1000) * 4 + 1; + + star.x = star.x; + star.y = star.y; + star.position.set(screenX, screenY); + star.scale.set(size); + star.alpha = 1 - star.z / 1000; + }); + }, + + cleanup(app, objects) { + objects.stars.forEach(s => s.destroy()); + } +}; + +// ============================================================================ +// DEMO 4: WAVE INTERFERENCE +// ============================================================================ + +export const waveInterference = { + name: "Wave Interference", + description: "Rippling wave patterns", + + setup(app, state) { + const gridSize = 20; + const cols = Math.floor(app.screen.width / gridSize); + const rows = Math.floor(app.screen.height / gridSize); + const circles = []; + + for (let i = 0; i < cols; i++) { + for (let j = 0; j < rows; j++) { + const circle = new PIXI.Graphics(); + circle.circle(0, 0, 4); + circle.fill(0x4ecdc4); + circle.x = i * gridSize + gridSize / 2; + circle.y = j * gridSize + gridSize / 2; + circle.baseX = circle.x; + circle.baseY = circle.y; + + app.stage.addChild(circle); + circles.push(circle); + } + } + + return { circles, sources: [ + { x: app.screen.width * 0.3, y: app.screen.height * 0.5 }, + { x: app.screen.width * 0.7, y: app.screen.height * 0.5 } + ]}; + }, + + update(app, state, objects) { + const t = state.state.time.current; + + objects.circles.forEach(c => { + let totalOffset = 0; + + objects.sources.forEach(source => { + const dx = c.baseX - source.x; + const dy = c.baseY - source.y; + const dist = Math.sqrt(dx * dx + dy * dy); + totalOffset += Math.sin(dist * 0.05 - t * 3) * 10; + }); + + c.y = c.baseY + totalOffset; + c.alpha = 0.3 + (Math.sin(totalOffset * 0.1) + 1) * 0.35; + }); + }, + + cleanup(app, objects) { + objects.circles.forEach(c => c.destroy()); + } +}; + +// ============================================================================ +// DEMO 5: CIRCLE PACKING +// ============================================================================ + +export const circlePacking = { + name: "Circle Packing", + description: "Organic growth simulation", + + setup(app, state) { + const circles = []; + return { circles, attempts: 0 }; + }, + + update(app, state, objects) { + // Try to add a new circle each frame + const maxAttempts = 100; + const maxCircles = 150; + + if (objects.circles.length >= maxCircles) return; + + for (let i = 0; i < 10; i++) { + const x = Math.random() * app.screen.width; + const y = Math.random() * app.screen.height; + const minRadius = 5; + const maxRadius = 60; + + let valid = true; + let radius = minRadius; + + // Find largest radius that doesn't overlap + for (let r = minRadius; r < maxRadius; r++) { + let overlaps = false; + + for (const other of objects.circles) { + const dx = x - other.x; + const dy = y - other.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < r + other.radius + 2) { + overlaps = true; + break; + } + } + + if (overlaps) { + break; + } + radius = r; + } + + if (radius > minRadius) { + const circle = new PIXI.Graphics(); + circle.circle(0, 0, radius); + const hue = (objects.circles.length * 137.5) % 360; + circle.fill(hslToHex(hue, 70, 60)); + circle.x = x; + circle.y = y; + circle.radius = radius; + + app.stage.addChild(circle); + objects.circles.push(circle); + break; + } + } + }, + + cleanup(app, objects) { + objects.circles.forEach(c => c.destroy()); + } +}; + +// ============================================================================ +// DEMO 6: PERLIN FLOW FIELD +// ============================================================================ + +export const flowField = { + name: "Flow Field", + description: "Particles following a noise field", + + setup(app, state) { + const particles = []; + const colors = [0xff6b6b, 0x4ecdc4, 0x45b7d1, 0xf9ca24, 0x6c5ce7, 0xfeca57]; + + for (let i = 0; i < 300; i++) { + const particle = new PIXI.Graphics(); + particle.circle(0, 0, 2); + particle.fill(colors[Math.floor(Math.random() * colors.length)]); + particle.alpha = 0.6; + + particle.x = Math.random() * app.screen.width; + particle.y = Math.random() * app.screen.height; + particle.vx = 0; + particle.vy = 0; + particle.color = colors[Math.floor(Math.random() * colors.length)]; + + app.stage.addChild(particle); + particles.push(particle); + } + + return { particles }; + }, + + update(app, state, objects) { + const t = state.state.time.current; + + objects.particles.forEach(p => { + // Simple noise-like function using sin/cos + const angle = noise(p.x * 0.005, p.y * 0.005, t * 0.3) * Math.PI * 2; + + p.vx += Math.cos(angle) * 0.3; + p.vy += Math.sin(angle) * 0.3; + + // Damping + p.vx *= 0.95; + p.vy *= 0.95; + + p.x += p.vx; + p.y += p.vy; + + // Wrap around screen + if (p.x < 0) p.x = app.screen.width; + if (p.x > app.screen.width) p.x = 0; + if (p.y < 0) p.y = app.screen.height; + if (p.y > app.screen.height) p.y = 0; + }); + }, + + cleanup(app, objects) { + objects.particles.forEach(p => p.destroy()); + } +}; + +// ============================================================================ +// DEMO 7: DNA HELIX +// ============================================================================ + +export const dnaHelix = { + name: "DNA Helix", + description: "Rotating double helix structure", + + setup(app, state) { + const helix1 = []; + const helix2 = []; + const connectors = []; + const segments = 40; + + for (let i = 0; i < segments; i++) { + const sphere1 = new PIXI.Graphics(); + sphere1.circle(0, 0, 8); + sphere1.fill(0x4ecdc4); + app.stage.addChild(sphere1); + helix1.push(sphere1); + + const sphere2 = new PIXI.Graphics(); + sphere2.circle(0, 0, 8); + sphere2.fill(0xff6b6b); + app.stage.addChild(sphere2); + helix2.push(sphere2); + + const connector = new PIXI.Graphics(); + app.stage.addChild(connector); + connectors.push(connector); + } + + return { helix1, helix2, connectors }; + }, + + update(app, state, objects) { + const t = state.state.time.current; + const cx = app.screen.width / 2; + const cy = app.screen.height / 2; + const radius = 100; + const height = app.screen.height * 0.8; + const spacing = height / objects.helix1.length; + + objects.helix1.forEach((sphere, i) => { + const y = i * spacing - height / 2 + cy; + const angle = t + i * 0.3; + const x = cx + Math.cos(angle) * radius; + const z = Math.sin(angle) * radius; + + sphere.x = x; + sphere.y = y; + sphere.scale.set(1 + z / 200); + sphere.alpha = 0.5 + z / 400; + }); + + objects.helix2.forEach((sphere, i) => { + const y = i * spacing - height / 2 + cy; + const angle = t + i * 0.3 + Math.PI; + const x = cx + Math.cos(angle) * radius; + const z = Math.sin(angle) * radius; + + sphere.x = x; + sphere.y = y; + sphere.scale.set(1 + z / 200); + sphere.alpha = 0.5 + z / 400; + }); + + // Draw connectors + objects.connectors.forEach((connector, i) => { + connector.clear(); + connector.moveTo(objects.helix1[i].x, objects.helix1[i].y); + connector.lineTo(objects.helix2[i].x, objects.helix2[i].y); + connector.stroke({ width: 2, color: 0x666666, alpha: 0.3 }); + }); + }, + + cleanup(app, objects) { + objects.helix1.forEach(s => s.destroy()); + objects.helix2.forEach(s => s.destroy()); + objects.connectors.forEach(c => c.destroy()); + } +}; + +// ============================================================================ +// DEMO 8: FIREWORKS +// ============================================================================ + +export const fireworks = { + name: "Fireworks", + description: "Explosive particle celebration", + + setup(app, state) { + return { + explosions: [], + nextExplosion: 0 + }; + }, + + update(app, state, objects) { + const t = state.state.time.current; + + // Create new explosion every second + if (t > objects.nextExplosion) { + objects.nextExplosion = t + 0.5 + Math.random(); + + const explosion = { + x: Math.random() * app.screen.width, + y: Math.random() * app.screen.height * 0.7, + particles: [], + color: Math.random() * 0xffffff, + born: t + }; + + // Create particles + for (let i = 0; i < 50; i++) { + const angle = (i / 50) * Math.PI * 2; + const speed = 2 + Math.random() * 4; + const particle = new PIXI.Graphics(); + particle.circle(0, 0, 3); + particle.fill(explosion.color); + particle.x = explosion.x; + particle.y = explosion.y; + particle.vx = Math.cos(angle) * speed; + particle.vy = Math.sin(angle) * speed; + + app.stage.addChild(particle); + explosion.particles.push(particle); + } + + objects.explosions.push(explosion); + } + + // Update explosions + objects.explosions = objects.explosions.filter(explosion => { + const age = t - explosion.born; + + if (age > 3) { + explosion.particles.forEach(p => p.destroy()); + return false; + } + + explosion.particles.forEach(p => { + p.vx *= 0.98; + p.vy += 0.1; // Gravity + p.x += p.vx; + p.y += p.vy; + p.alpha = 1 - age / 3; + }); + + return true; + }); + }, + + cleanup(app, objects) { + objects.explosions.forEach(explosion => { + explosion.particles.forEach(p => p.destroy()); + }); + } +}; + +// ============================================================================ +// DEMO 9: MATRIX RAIN +// ============================================================================ + +export const matrixRain = { + name: "Matrix Rain", + description: "Falling digital rain effect", + + setup(app, state) { + const fontSize = 16; + const columns = Math.floor(app.screen.width / fontSize); + const drops = []; + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*"; + + for (let i = 0; i < columns; i++) { + const text = new PIXI.Text('', { + fontFamily: 'monospace', + fontSize: fontSize, + fill: 0x00ff00 + }); + text.x = i * fontSize; + text.y = -Math.random() * app.screen.height; + + app.stage.addChild(text); + drops.push({ + text, + speed: 1 + Math.random() * 3, + chars: chars + }); + } + + return { drops }; + }, + + update(app, state, objects) { + objects.drops.forEach(drop => { + drop.y = (drop.y || drop.text.y) + drop.speed; + drop.text.y = drop.y; + + // Random character + if (Math.random() > 0.95) { + drop.text.text = drop.chars[Math.floor(Math.random() * drop.chars.length)]; + } + + // Reset to top + if (drop.y > app.screen.height) { + drop.y = -20; + drop.text.alpha = 1; + } + + // Fade trail + drop.text.alpha = Math.max(0.1, drop.text.alpha - 0.01); + }); + }, + + cleanup(app, objects) { + objects.drops.forEach(d => d.text.destroy()); + } +}; + +// ============================================================================ +// DEMO 10: SOLAR SYSTEM +// ============================================================================ + +export const solarSystem = { + name: "Solar System", + description: "Orbiting planets around a star", + + setup(app, state) { + const cx = app.screen.width / 2; + const cy = app.screen.height / 2; + + // Sun + const sun = new PIXI.Graphics(); + sun.circle(0, 0, 30); + sun.fill(0xffd700); + sun.x = cx; + sun.y = cy; + app.stage.addChild(sun); + + // Planets + const planets = [ + { radius: 60, size: 6, speed: 2.0, color: 0x8b7355 }, + { radius: 100, size: 10, speed: 1.5, color: 0xff6347 }, + { radius: 150, size: 12, speed: 1.0, color: 0x4169e1 }, + { radius: 200, size: 8, speed: 0.7, color: 0xff4500 }, + { radius: 260, size: 18, speed: 0.4, color: 0xdaa520 }, + ]; + + const planetObjects = planets.map(config => { + const planet = new PIXI.Graphics(); + planet.circle(0, 0, config.size); + planet.fill(config.color); + planet.config = config; + app.stage.addChild(planet); + return planet; + }); + + return { sun, planets: planetObjects, cx, cy }; + }, + + update(app, state, objects) { + const t = state.state.time.current; + + objects.planets.forEach((planet, i) => { + const angle = t * planet.config.speed; + planet.x = objects.cx + Math.cos(angle) * planet.config.radius; + planet.y = objects.cy + Math.sin(angle) * planet.config.radius; + }); + }, + + cleanup(app, objects) { + objects.sun.destroy(); + objects.planets.forEach(p => p.destroy()); + } +}; + +// ============================================================================ +// UTILITIES +// ============================================================================ + +function hslToHex(h, s, l) { + s /= 100; + l /= 100; + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs((h / 60) % 2 - 1)); + const m = l - c/2; + let r = 0, g = 0, b = 0; + + if (0 <= h && h < 60) { + r = c; g = x; b = 0; + } else if (60 <= h && h < 120) { + r = x; g = c; b = 0; + } else if (120 <= h && h < 180) { + r = 0; g = c; b = x; + } else if (180 <= h && h < 240) { + r = 0; g = x; b = c; + } else if (240 <= h && h < 300) { + r = x; g = 0; b = c; + } else if (300 <= h && h < 360) { + r = c; g = 0; b = x; + } + + r = Math.round((r + m) * 255); + g = Math.round((g + m) * 255); + b = Math.round((b + m) * 255); + + return (r << 16) | (g << 8) | b; +} + +function noise(x, y, z) { + return Math.sin(x + Math.cos(y)) * Math.cos(y + Math.sin(z)) * Math.sin(z + Math.cos(x)); +} + +// ============================================================================ +// EXPORT ALL DEMOS +// ============================================================================ + +export const allDemos = [ + bouncingParticles, + spirograph, + starfield, + waveInterference, + circlePacking, + flowField, + dnaHelix, + fireworks, + matrixRain, + solarSystem +]; diff --git a/src/example-usage.js b/src/example-usage.js new file mode 100644 index 0000000..67eff4b --- /dev/null +++ b/src/example-usage.js @@ -0,0 +1,535 @@ +/** + * Example Usage: Complete examples of the new architecture + * + * This file demonstrates how to use the separated data/visualization architecture: + * - TimeSeriesPlot: Pure visualization + * - DataSource: Data generation/provision + * - Connections: Links between them + */ + +import { Application } from 'pixi.js'; +import { TimeSeriesPlot } from './timeseries-plot.js'; +import { + SyntheticDataSource, + FunctionDataSource, + StreamingDataSource, + WebSocketDataSource, +} from './data-sources.js'; +import { + DirectConnection, + BufferedConnection, + ConnectionManager, + connectSyntheticData, + connectFunction, + createConnectedPlot, +} from './plot-connections.js'; +import { + TestDataFactory, + SineWaveGenerator, + PerlinNoiseGenerator, + ChirpGenerator, +} from './test-data-generators.js'; + +// ============================================================================ +// Example 1: Simple Setup - One plot, one data source +// ============================================================================ + +export async function example1_SimpleSetup() { + console.log('=== Example 1: Simple Setup ==='); + + // Create PixiJS app + const app = new Application(); + await app.init({ + width: 800, + height: 600, + backgroundColor: 0x1a1a26, + }); + document.body.appendChild(app.canvas); + + // Create plot (visualization only) + const plot = new TimeSeriesPlot({ + x: 0, + y: 0, + width: 800, + height: 600, + title: 'Simple Sine Wave', + showGrid: true, + }); + app.stage.addChild(plot.container); + + // Create data source + const generator = TestDataFactory.createSimpleSine(30); + const source = new SyntheticDataSource({ + generator: generator, + pointsPerLine: 100, + width: 800, + lineInterval: 100, // New line every 100ms + }); + + // Connect source to plot + const connection = new DirectConnection(source, plot); + connection.connect(); + + // Update plot every frame + app.ticker.add(() => { + plot.update(); + }); + + return { app, plot, source, connection }; +} + +// ============================================================================ +// Example 2: Quick Setup Using Helper Functions +// ============================================================================ + +export async function example2_QuickSetup() { + console.log('=== Example 2: Quick Setup ==='); + + const app = new Application(); + await app.init({ + width: 800, + height: 600, + backgroundColor: 0x1a1a26, + }); + document.body.appendChild(app.canvas); + + // One-liner setup! + const { plot, source, connection } = createConnectedPlot( + app, + { + x: 0, + y: 0, + width: 800, + height: 600, + title: 'Quick Setup', + }, + { + generator: TestDataFactory.createComplexPattern(30), + lineInterval: 100, + } + ); + + app.ticker.add(() => plot.update()); + + return { app, plot, source, connection }; +} + +// ============================================================================ +// Example 3: Multiple Plots with Different Data Sources +// ============================================================================ + +export async function example3_MultiplePlots() { + console.log('=== Example 3: Multiple Plots ==='); + + const app = new Application(); + await app.init({ + width: 1600, + height: 600, + backgroundColor: 0x1a1a26, + }); + document.body.appendChild(app.canvas); + + const width = 800; + const height = 600; + + // Left plot: Sine wave + const plot1 = new TimeSeriesPlot({ + x: 0, + y: 0, + width: width, + height: height, + title: 'Sine Wave', + color: 0xff6666, + }); + + // Right plot: Perlin noise + const plot2 = new TimeSeriesPlot({ + x: width, + y: 0, + width: width, + height: height, + title: 'Perlin Noise', + color: 0x66ff66, + }); + + app.stage.addChild(plot1.container); + app.stage.addChild(plot2.container); + + // Connect different data sources + const conn1 = connectSyntheticData( + TestDataFactory.createSimpleSine(30), + plot1, + { lineInterval: 100 } + ); + + const conn2 = connectSyntheticData( + TestDataFactory.createSmoothNoise(30), + plot2, + { lineInterval: 100 } + ); + + app.ticker.add(() => { + plot1.update(); + plot2.update(); + }); + + return { app, plots: [plot1, plot2], connections: [conn1, conn2] }; +} + +// ============================================================================ +// Example 4: Using Function-Based Data Source +// ============================================================================ + +export async function example4_FunctionSource() { + console.log('=== Example 4: Function Source ==='); + + const app = new Application(); + await app.init({ width: 800, height: 600, backgroundColor: 0x1a1a26 }); + document.body.appendChild(app.canvas); + + const plot = new TimeSeriesPlot({ + x: 0, + y: 0, + width: 800, + height: 600, + title: 'Custom Function', + }); + app.stage.addChild(plot.container); + + // Define a custom function: (x, t) => y + // x is normalized 0-1 across the width + // t is time in seconds + const customFunc = (x, t) => { + // Create an interference pattern + const wave1 = Math.sin(x * 10 + t * 2); + const wave2 = Math.sin(x * 15 - t * 3); + const wave3 = Math.cos(x * 8 + t * 1.5); + return (wave1 + wave2 + wave3) / 3; + }; + + const connection = connectFunction(customFunc, plot, { + lineInterval: 100, + amplitude: 30, + }); + + app.ticker.add(() => plot.update()); + + return { app, plot, connection }; +} + +// ============================================================================ +// Example 5: Swapping Data Sources at Runtime +// ============================================================================ + +export async function example5_SwappingSources() { + console.log('=== Example 5: Swapping Sources ==='); + + const app = new Application(); + await app.init({ width: 800, height: 600, backgroundColor: 0x1a1a26 }); + document.body.appendChild(app.canvas); + + const plot = new TimeSeriesPlot({ + x: 0, + y: 0, + width: 800, + height: 600, + title: 'Dynamic Source Switching', + }); + app.stage.addChild(plot.container); + + // Start with sine wave + let currentConnection = connectSyntheticData( + TestDataFactory.createSimpleSine(30), + plot, + { lineInterval: 100 } + ); + + app.ticker.add(() => plot.update()); + + // Function to switch to a different data source + const switchToSource = (generator, title) => { + // Disconnect current source + currentConnection.disconnect(); + + // Connect new source + currentConnection = connectSyntheticData(generator, plot, { + lineInterval: 100, + }); + + plot.setTitle(title); + console.log(`Switched to: ${title}`); + }; + + // Example: Switch sources every 5 seconds + let sourceIndex = 0; + const sources = [ + { gen: TestDataFactory.createSimpleSine(30), title: 'Sine Wave' }, + { gen: TestDataFactory.createComplexPattern(30), title: 'Complex Pattern' }, + { gen: TestDataFactory.createSmoothNoise(30), title: 'Perlin Noise' }, + { gen: TestDataFactory.createFrequencySweep(30), title: 'Frequency Sweep' }, + ]; + + setInterval(() => { + sourceIndex = (sourceIndex + 1) % sources.length; + const source = sources[sourceIndex]; + switchToSource(source.gen, source.title); + }, 5000); + + return { app, plot, switchToSource }; +} + +// ============================================================================ +// Example 6: Streaming Data with Buffering +// ============================================================================ + +export async function example6_StreamingData() { + console.log('=== Example 6: Streaming Data ==='); + + const app = new Application(); + await app.init({ width: 800, height: 600, backgroundColor: 0x1a1a26 }); + document.body.appendChild(app.canvas); + + const plot = new TimeSeriesPlot({ + x: 0, + y: 0, + width: 800, + height: 600, + title: 'Streaming Data (Buffered)', + }); + app.stage.addChild(plot.container); + + // Create streaming source (emits individual points) + const generator = new SineWaveGenerator({ + frequency: 2.0, + amplitude: 1.0, + sampleRate: 60, + }); + + const source = new StreamingDataSource({ + generator: generator, + sampleRate: 60, // 60 points per second + }); + + // Use buffered connection to assemble points into lines + const connection = new BufferedConnection(source, plot, { + bufferSize: 100, // Buffer 100 points before creating a line + bufferTimeout: 1000, // Or timeout after 1 second + }); + connection.connect(); + + app.ticker.add(() => plot.update()); + + return { app, plot, source, connection }; +} + +// ============================================================================ +// Example 7: Connection Manager (Managing Multiple Connections) +// ============================================================================ + +export async function example7_ConnectionManager() { + console.log('=== Example 7: Connection Manager ==='); + + const app = new Application(); + await app.init({ width: 800, height: 600, backgroundColor: 0x1a1a26 }); + document.body.appendChild(app.canvas); + + const plot = new TimeSeriesPlot({ + x: 0, + y: 0, + width: 800, + height: 600, + title: 'Managed Connections', + }); + app.stage.addChild(plot.container); + + // Create connection manager + const manager = new ConnectionManager(); + + // Add first connection + const source1 = new SyntheticDataSource({ + generator: TestDataFactory.createSimpleSine(30), + pointsPerLine: 100, + width: 800, + lineInterval: 100, + }); + + const connId1 = manager.connect(source1, plot, { type: 'direct' }); + console.log('Connection ID:', connId1); + + app.ticker.add(() => plot.update()); + + // Later: disconnect and switch to different source + setTimeout(() => { + manager.disconnect(connId1); + + const source2 = new SyntheticDataSource({ + generator: TestDataFactory.createFrequencySweep(30), + pointsPerLine: 100, + width: 800, + lineInterval: 100, + }); + + const connId2 = manager.connect(source2, plot, { type: 'direct' }); + plot.setTitle('Frequency Sweep'); + console.log('Switched to connection:', connId2); + }, 5000); + + return { app, plot, manager }; +} + +// ============================================================================ +// Example 8: Complete Interactive Demo +// ============================================================================ + +export async function example8_InteractiveDemo() { + console.log('=== Example 8: Interactive Demo ==='); + + const app = new Application(); + await app.init({ + width: 1600, + height: 800, + backgroundColor: 0x1a1a26, + }); + document.body.appendChild(app.canvas); + + // Create two plots + const plot1 = new TimeSeriesPlot({ + x: 0, + y: 0, + width: 800, + height: 800, + title: 'Plot 1 - Press 1-5 to change', + color: 0xff6666, + }); + + const plot2 = new TimeSeriesPlot({ + x: 800, + y: 0, + width: 800, + height: 800, + title: 'Plot 2 - Press 6-0 to change', + color: 0x66ff66, + }); + + app.stage.addChild(plot1.container); + app.stage.addChild(plot2.container); + + // Connection manager + const manager = new ConnectionManager(); + + // Available data sources + const dataSources = { + sine: () => TestDataFactory.createSimpleSine(30), + complex: () => TestDataFactory.createComplexPattern(30), + noise: () => TestDataFactory.createSmoothNoise(30), + sweep: () => TestDataFactory.createFrequencySweep(30), + burst: () => TestDataFactory.createBurstySignal(30), + }; + + // Track current connections + let conn1Id = null; + let conn2Id = null; + + // Helper to switch source + const switchSource = (plot, generatorFunc, title) => { + // Disconnect old connection + const connId = plot === plot1 ? conn1Id : conn2Id; + if (connId !== null) { + manager.disconnect(connId); + } + + // Create new connection + const source = new SyntheticDataSource({ + generator: generatorFunc(), + pointsPerLine: 100, + width: plot.width, + lineInterval: 100, + }); + + const newConnId = manager.connect(source, plot, { type: 'direct' }); + plot.setTitle(title); + + // Store connection ID + if (plot === plot1) { + conn1Id = newConnId; + } else { + conn2Id = newConnId; + } + }; + + // Initialize with default sources + switchSource(plot1, dataSources.sine, 'Plot 1 - Sine Wave'); + switchSource(plot2, dataSources.complex, 'Plot 2 - Complex Pattern'); + + // Keyboard controls + window.addEventListener('keydown', (e) => { + switch (e.key) { + case '1': + switchSource(plot1, dataSources.sine, 'Plot 1 - Sine Wave'); + break; + case '2': + switchSource(plot1, dataSources.complex, 'Plot 1 - Complex Pattern'); + break; + case '3': + switchSource(plot1, dataSources.noise, 'Plot 1 - Perlin Noise'); + break; + case '4': + switchSource(plot1, dataSources.sweep, 'Plot 1 - Frequency Sweep'); + break; + case '5': + switchSource(plot1, dataSources.burst, 'Plot 1 - Burst Signal'); + break; + case '6': + switchSource(plot2, dataSources.sine, 'Plot 2 - Sine Wave'); + break; + case '7': + switchSource(plot2, dataSources.complex, 'Plot 2 - Complex Pattern'); + break; + case '8': + switchSource(plot2, dataSources.noise, 'Plot 2 - Perlin Noise'); + break; + case '9': + switchSource(plot2, dataSources.sweep, 'Plot 2 - Frequency Sweep'); + break; + case '0': + switchSource(plot2, dataSources.burst, 'Plot 2 - Burst Signal'); + break; + case 'g': + plot1.setGridVisible(!plot1.showGrid); + plot2.setGridVisible(!plot2.showGrid); + break; + case 'c': + plot1.clearData(); + plot2.clearData(); + break; + } + }); + + // Update loop + app.ticker.add(() => { + plot1.update(); + plot2.update(); + }); + + console.log('Controls:'); + console.log(' 1-5: Change Plot 1 source'); + console.log(' 6-0: Change Plot 2 source'); + console.log(' G: Toggle grid'); + console.log(' C: Clear data'); + + return { app, plot1, plot2, manager }; +} + +// ============================================================================ +// Quick Test: Run one of the examples +// ============================================================================ + +// Uncomment to run an example: +// example1_SimpleSetup(); +// example2_QuickSetup(); +// example3_MultiplePlots(); +// example4_FunctionSource(); +// example5_SwappingSources(); +// example6_StreamingData(); +// example7_ConnectionManager(); +//example8_InteractiveDemo(); diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..d2b348e --- /dev/null +++ b/src/main.js @@ -0,0 +1 @@ +import './bootstrap.js'; diff --git a/src/metrics.js b/src/metrics.js new file mode 100644 index 0000000..fdda10a --- /dev/null +++ b/src/metrics.js @@ -0,0 +1,142 @@ +/** + * RollingAverage - Maintains a rolling window of values for smooth averaging + */ +class RollingAverage { + constructor(capacity) { + this.values = []; + this.capacity = capacity; + this.sum = 0; + } + + push(value) { + if (this.values.length >= this.capacity) { + const old = this.values.shift(); + this.sum -= old; + } + this.values.push(value); + this.sum += value; + } + + average() { + return this.values.length > 0 ? this.sum / this.values.length : 0; + } + + min() { + return this.values.length > 0 ? Math.min(...this.values) : 0; + } + + max() { + return this.values.length > 0 ? Math.max(...this.values) : 0; + } +} + +/** + * PerformanceMetrics - Tracks and analyzes frame performance + */ +export class PerformanceMetrics { + constructor(rollingWindow = 60, historyCapacity = 10000) { + // Rolling averages + this.frameTime = new RollingAverage(rollingWindow); + this.updateTime = new RollingAverage(rollingWindow); + this.renderTime = new RollingAverage(rollingWindow); + this.vertexCount = new RollingAverage(rollingWindow); + this.lineCount = new RollingAverage(rollingWindow); + + // History for export + this.history = []; + this.historyCapacity = historyCapacity; + + // Frame timing + this.frameStart = 0; + this.updateStart = 0; + this.renderStart = 0; + + this.totalFrames = 0; + } + + beginFrame() { + this.frameStart = performance.now(); + } + + beginUpdate() { + this.updateStart = performance.now(); + } + + endUpdate() { + const duration = performance.now() - this.updateStart; + return duration; + } + + beginRender() { + this.renderStart = performance.now(); + } + + endRender() { + const duration = performance.now() - this.renderStart; + return duration; + } + + endFrame(updateMs, renderMs, vertexCount, lineCount) { + const totalMs = performance.now() - this.frameStart; + + // Update rolling averages + this.frameTime.push(totalMs); + this.updateTime.push(updateMs); + this.renderTime.push(renderMs); + this.vertexCount.push(vertexCount); + this.lineCount.push(lineCount); + + // Store in history + const record = { + frame: this.totalFrames, + totalMs, + updateMs, + renderMs, + vertexCount, + lineCount, + fps: totalMs > 0 ? 1000 / totalMs : 0, + }; + + if (this.history.length >= this.historyCapacity) { + this.history.shift(); + } + this.history.push(record); + + this.totalFrames++; + } + + getFPS() { + const avg = this.frameTime.average(); + return avg > 0 ? 1000 / avg : 0; + } + + getMinFPS() { + const max = this.frameTime.max(); + return max > 0 ? 1000 / max : 0; + } + + getMaxFPS() { + const min = this.frameTime.min(); + return min > 0 ? 1000 / min : 0; + } + + formatSummary() { + return `FPS: ${this.getFPS().toFixed(1)} (min: ${this.getMinFPS().toFixed(1)}, max: ${this.getMaxFPS().toFixed(1)}) | ` + + `Frame: ${this.frameTime.average().toFixed(2)}ms | ` + + `Update: ${this.updateTime.average().toFixed(2)}ms | ` + + `Render: ${this.renderTime.average().toFixed(2)}ms | ` + + `Vertices: ${Math.round(this.vertexCount.average())} | ` + + `Lines: ${Math.round(this.lineCount.average())}`; + } + + exportToCSV() { + let csv = 'frame,total_ms,update_ms,render_ms,vertex_count,line_count,fps\n'; + + for (const record of this.history) { + csv += `${record.frame},${record.totalMs},${record.updateMs},${record.renderMs},` + + `${record.vertexCount},${record.lineCount},${record.fps}\n`; + } + + return csv; + } +} diff --git a/src/plot-connections.js b/src/plot-connections.js new file mode 100644 index 0000000..0e96dd8 --- /dev/null +++ b/src/plot-connections.js @@ -0,0 +1,392 @@ +/** + * Plot Connections - Links data sources to visualization plots + * + * This module manages the connection between data sources and plots, + * handling buffering, timing, and data flow. + * + * Connection Types: + * - DirectConnection: Lines from source → plot (no buffering) + * - BufferedConnection: Points → buffer → lines → plot + * - SynchronizedConnection: Multiple sources → synchronized output + */ + +/** + * Base connection class + */ +class PlotConnection { + constructor(source, plot, config = {}) { + this.source = source; + this.plot = plot; + this.config = config; + this.isActive = false; + this.subscriptions = []; + } + + /** + * Activate the connection - start data flow + */ + connect() { + if (this.isActive) return; + this.isActive = true; + this.setupSubscriptions(); + this.source.start(); + } + + /** + * Deactivate the connection - stop data flow + */ + disconnect() { + if (!this.isActive) return; + this.isActive = false; + this.cleanup(); + this.source.stop(); + } + + /** + * Setup event subscriptions (override in subclasses) + */ + setupSubscriptions() { + throw new Error('setupSubscriptions() must be implemented by subclass'); + } + + /** + * Cleanup subscriptions + */ + cleanup() { + this.subscriptions.forEach(unsub => unsub()); + this.subscriptions = []; + } +} + +/** + * Direct connection - passes lines directly from source to plot + * Use when source emits complete lines of data + */ +export class DirectConnection extends PlotConnection { + setupSubscriptions() { + const unsubLine = this.source.on('line', (data) => { + this.plot.addLine(data.points, data.metadata); + }); + + const unsubError = this.source.on('error', (data) => { + console.error('[DirectConnection] Source error:', data.error); + }); + + this.subscriptions.push(unsubLine, unsubError); + } +} + +/** + * Buffered connection - buffers individual points into lines + * Use when source emits individual data points that need to be assembled + */ +export class BufferedConnection extends PlotConnection { + constructor(source, plot, config = {}) { + super(source, plot, config); + this.buffer = []; + this.bufferSize = config.bufferSize || 100; + this.bufferTimeout = config.bufferTimeout || 1000; // ms + this.lastFlush = Date.now(); + this.flushHandle = null; + + // Start auto-flush timer + if (config.autoFlush !== false) { + this.startAutoFlush(); + } + } + + setupSubscriptions() { + const unsubPoint = this.source.on('point', (data) => { + this.addToBuffer(data); + }); + + const unsubError = this.source.on('error', (data) => { + console.error('[BufferedConnection] Source error:', data.error); + }); + + this.subscriptions.push(unsubPoint, unsubError); + } + + addToBuffer(data) { + this.buffer.push(data); + + // Flush if buffer is full + if (this.buffer.length >= this.bufferSize) { + this.flush(); + } + } + + flush() { + if (this.buffer.length === 0) return; + + // Convert buffer to line points + const points = this.buffer.map((data, idx) => { + const x = (idx / this.buffer.length) * this.plot.width; + return { x, y: data.value }; + }); + + this.plot.addLine(points, { + timestamp: this.lastFlush, + pointCount: this.buffer.length, + }); + + this.buffer = []; + this.lastFlush = Date.now(); + } + + startAutoFlush() { + this.flushHandle = setInterval(() => { + const timeSinceLastFlush = Date.now() - this.lastFlush; + if (timeSinceLastFlush >= this.bufferTimeout && this.buffer.length > 0) { + this.flush(); + } + }, 100); // Check every 100ms + } + + cleanup() { + super.cleanup(); + if (this.flushHandle) { + clearInterval(this.flushHandle); + this.flushHandle = null; + } + } +} + +/** + * Synchronized connection - synchronizes multiple sources to one plot + * Useful for combining multiple data streams + */ +export class SynchronizedConnection extends PlotConnection { + constructor(sources, plot, config = {}) { + super(null, plot, config); // No single source + this.sources = sources; + this.syncMode = config.syncMode || 'wait-for-all'; // 'wait-for-all', 'first-available' + this.lineBuffers = new Map(); // sourceId => latest line + } + + connect() { + if (this.isActive) return; + this.isActive = true; + + this.sources.forEach((source, idx) => { + const unsubLine = source.on('line', (data) => { + this.handleSourceLine(idx, data); + }); + + const unsubError = source.on('error', (data) => { + console.error(`[SynchronizedConnection] Source ${idx} error:`, data.error); + }); + + this.subscriptions.push(unsubLine, unsubError); + source.start(); + }); + } + + disconnect() { + if (!this.isActive) return; + this.isActive = false; + this.cleanup(); + this.sources.forEach(source => source.stop()); + } + + handleSourceLine(sourceIdx, data) { + this.lineBuffers.set(sourceIdx, data); + + if (this.syncMode === 'wait-for-all') { + // Wait until we have data from all sources + if (this.lineBuffers.size === this.sources.length) { + this.emitSynchronized(); + } + } else if (this.syncMode === 'first-available') { + // Emit immediately + this.plot.addLine(data.points, { + ...data.metadata, + sourceIdx, + }); + } + } + + emitSynchronized() { + // For now, just emit the first source's line + // Could implement more sophisticated merging + const firstLine = this.lineBuffers.get(0); + if (firstLine) { + this.plot.addLine(firstLine.points, firstLine.metadata); + } + this.lineBuffers.clear(); + } +} + +/** + * Connection Manager - manages multiple connections + */ +export class ConnectionManager { + constructor() { + this.connections = new Map(); // connectionId => connection + this.nextId = 0; + } + + /** + * Create and register a connection + * @returns {number} connectionId + */ + connect(source, plot, config = {}) { + const type = config.type || 'direct'; + let connection; + + switch (type) { + case 'direct': + connection = new DirectConnection(source, plot, config); + break; + case 'buffered': + connection = new BufferedConnection(source, plot, config); + break; + case 'synchronized': + connection = new SynchronizedConnection(source, plot, config); + break; + default: + throw new Error(`Unknown connection type: ${type}`); + } + + const id = this.nextId++; + this.connections.set(id, connection); + connection.connect(); + + return id; + } + + /** + * Disconnect and remove a connection + */ + disconnect(connectionId) { + const connection = this.connections.get(connectionId); + if (connection) { + connection.disconnect(); + this.connections.delete(connectionId); + } + } + + /** + * Disconnect all connections + */ + disconnectAll() { + this.connections.forEach(connection => connection.disconnect()); + this.connections.clear(); + } + + /** + * Get statistics about connections + */ + getStats() { + return { + activeConnections: this.connections.size, + connections: Array.from(this.connections.entries()).map(([id, conn]) => ({ + id, + isActive: conn.isActive, + type: conn.constructor.name, + })), + }; + } +} + +/** + * Helper functions for common connection patterns + */ + +/** + * Connect a synthetic data source to a plot + * @param {DataGenerator} generator - Test data generator instance + * @param {TimeSeriesPlot} plot - Plot to display data + * @param {Object} config - Configuration options + * @returns {DirectConnection} The connection instance + */ +export function connectSyntheticData(generator, plot, config = {}) { + const { SyntheticDataSource } = require('./data-sources.js'); + + const source = new SyntheticDataSource({ + generator, + pointsPerLine: config.pointsPerLine || 100, + width: plot.width, + lineInterval: config.lineInterval || 100, + }); + + const connection = new DirectConnection(source, plot, config); + connection.connect(); + + return connection; +} + +/** + * Connect a function-based source to a plot + * @param {Function} func - Function (x, t) => y + * @param {TimeSeriesPlot} plot - Plot to display data + * @param {Object} config - Configuration options + * @returns {DirectConnection} The connection instance + */ +export function connectFunction(func, plot, config = {}) { + const { FunctionDataSource } = require('./data-sources.js'); + + const source = new FunctionDataSource({ + func, + pointsPerLine: config.pointsPerLine || 100, + width: plot.width, + amplitude: config.amplitude || 30, + lineInterval: config.lineInterval || 100, + }); + + const connection = new DirectConnection(source, plot, config); + connection.connect(); + + return connection; +} + +/** + * Connect a streaming source to a plot with buffering + * @param {DataGenerator} generator - Test data generator instance + * @param {TimeSeriesPlot} plot - Plot to display data + * @param {Object} config - Configuration options + * @returns {BufferedConnection} The connection instance + */ +export function connectStreamingData(generator, plot, config = {}) { + const { StreamingDataSource } = require('./data-sources.js'); + + const source = new StreamingDataSource({ + generator, + sampleRate: config.sampleRate || 60, + }); + + const connection = new BufferedConnection(source, plot, { + bufferSize: config.bufferSize || 100, + bufferTimeout: config.bufferTimeout || 1000, + }); + connection.connect(); + + return connection; +} + +/** + * Quick setup: Create a plot with a data source in one call + * @param {Application} app - PixiJS application + * @param {Object} plotConfig - Plot configuration + * @param {Object} sourceConfig - Source configuration + * @returns {Object} {plot, source, connection} + */ +export function createConnectedPlot(app, plotConfig, sourceConfig) { + const { TimeSeriesPlot } = require('./timeseries-plot.js'); + const { SyntheticDataSource } = require('./data-sources.js'); + + const plot = new TimeSeriesPlot(plotConfig); + app.stage.addChild(plot.container); + + const source = new SyntheticDataSource({ + generator: sourceConfig.generator, + pointsPerLine: plotConfig.width / 8, // Default: ~8 pixels per point + width: plotConfig.width, + lineInterval: sourceConfig.lineInterval || 100, + }); + + const connection = new DirectConnection(source, plot); + connection.connect(); + + return { plot, source, connection }; +} diff --git a/src/plot/plot-buffer.js b/src/plot/plot-buffer.js new file mode 100644 index 0000000..b13cdd8 --- /dev/null +++ b/src/plot/plot-buffer.js @@ -0,0 +1,22 @@ +export class PlotBuffer { + constructor(maxPoints = 1600) { + this.maxPoints = maxPoints; + this.points = []; + } + + addPoint(point) { + this.points.push(point); + if (this.points.length > this.maxPoints) { + this.points.splice(0, this.points.length - this.maxPoints); + } + } + + clear() { + this.points = []; + } + + getVisiblePoints(currentPlotTimeMs, windowDurationMs) { + const minTime = currentPlotTimeMs - windowDurationMs; + return this.points.filter((point) => point.timeMs >= minTime && point.timeMs <= currentPlotTimeMs); + } +} diff --git a/src/plot/timeplot-view.js b/src/plot/timeplot-view.js new file mode 100644 index 0000000..ce90a1f --- /dev/null +++ b/src/plot/timeplot-view.js @@ -0,0 +1,442 @@ +import { Application, Container, Graphics, Text } from 'pixi.js'; +import { formatDuration, formatValue, formatWallClock } from '../utils-format.js'; + +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + +function roundRect(graphics, x, y, width, height, radius, fill, stroke) { + graphics.roundRect(x, y, width, height, radius); + graphics.fill(fill); + graphics.stroke(stroke); +} + +export class TimeplotView { + constructor({ host, panelId = 'primary', title = 'Vertical plot', subtitle = null, showReadouts = true, lineColor = 0x9fd1ff, pointColor = 0xe7f2ff }) { + this.host = host; + this.panelId = panelId; + this.panelTitle = title; + this.panelSubtitle = subtitle; + this.showReadouts = showReadouts; + this.lineColor = lineColor; + this.pointColor = pointColor; + this.app = new Application(); + this.container = new Container(); + this.background = new Graphics(); + this.grid = new Graphics(); + this.axes = new Graphics(); + this.line = new Graphics(); + this.points = new Graphics(); + this.crosshair = new Graphics(); + this.overlay = new Container(); + this.readoutBackground = new Graphics(); + this.axisLabelLayer = new Container(); + this.titleText = new Text({ + text: 'Plot viewport', + style: { + fill: 0xeef4ff, + fontFamily: 'Inter, sans-serif', + fontSize: 16, + }, + }); + this.subtitleText = new Text({ + text: 'Synthetic data stream', + style: { + fill: 0x8ca3c7, + fontFamily: 'Inter, sans-serif', + fontSize: 12, + }, + }); + this.realTimeText = new Text({ + text: '', + style: { + fill: 0xe8eef7, + fontFamily: 'IBM Plex Mono, monospace', + fontSize: 11, + }, + }); + this.plotTimeText = new Text({ + text: '', + style: { + fill: 0xe8eef7, + fontFamily: 'IBM Plex Mono, monospace', + fontSize: 11, + }, + }); + this.axisTitleText = new Text({ + text: '', + style: { + fill: 0x90a0b7, + fontFamily: 'Inter, sans-serif', + fontSize: 10, + fontWeight: '600', + letterSpacing: 1.5, + }, + }); + this.screenPoints = []; + this.bounds = { width: 100, height: 100 }; + this.hoverRadiusPx = 20; + this.pointer = null; + this.lastPointerEventAt = 0; + this.axisLabels = []; + } + + async init() { + const rendererPreference = navigator.gpu ? 'webgpu' : 'webgl'; + await this.app.init({ + preference: rendererPreference, + resizeTo: this.host, + antialias: true, + backgroundAlpha: 0, + resolution: Math.min(window.devicePixelRatio || 1, 2), + }); + + this.app.stage.addChild(this.container); + this.container.addChild(this.background); + this.container.addChild(this.grid); + this.container.addChild(this.axes); + this.container.addChild(this.line); + this.container.addChild(this.points); + this.container.addChild(this.crosshair); + this.container.addChild(this.overlay); + this.overlay.addChild(this.readoutBackground); + this.overlay.addChild(this.axisLabelLayer); + this.overlay.addChild(this.titleText); + this.overlay.addChild(this.subtitleText); + this.overlay.addChild(this.realTimeText); + this.overlay.addChild(this.plotTimeText); + this.overlay.addChild(this.axisTitleText); + this.host.appendChild(this.app.canvas); + this.attachPointerListeners(); + + return rendererPreference; + } + + attachPointerListeners() { + this.host.addEventListener('pointerleave', () => { + this.pointer = null; + this.lastPointerEventAt = performance.now(); + }); + + this.host.addEventListener('pointermove', (event) => { + const rect = this.host.getBoundingClientRect(); + this.pointer = { + x: event.clientX - rect.left, + y: event.clientY - rect.top, + }; + this.lastPointerEventAt = performance.now(); + }); + } + + resize() { + this.bounds = { + width: this.host.clientWidth, + height: this.host.clientHeight, + }; + } + + render(state, points) { + this.resize(); + this.renderFrame(state, points); + this.clearHover(); + } + + clearHover() { + this.crosshair.clear(); + } + + getHoverCandidate() { + if (!this.pointer || this.screenPoints.length === 0) { + return null; + } + + let nearestPoint = null; + let nearestDistance = Infinity; + + for (const point of this.screenPoints) { + const dx = point.x - this.pointer.x; + const dy = point.y - this.pointer.y; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance < nearestDistance) { + nearestPoint = point; + nearestDistance = distance; + } + } + + if (!nearestPoint || nearestDistance > this.hoverRadiusPx) { + return null; + } + + return { + panelId: this.panelId, + point: nearestPoint, + x: clamp(nearestPoint.x, 0, this.bounds.width), + y: clamp(nearestPoint.y, 0, this.bounds.height), + pointerX: this.pointer.x, + pointerY: this.pointer.y, + distance: nearestDistance, + lastPointerEventAt: this.lastPointerEventAt, + }; + } + + hasPointer() { + return this.pointer !== null; + } + + findNearestScreenPointByTime(timeMs) { + if (this.screenPoints.length === 0) { + return null; + } + + let nearestPoint = null; + let nearestDelta = Infinity; + + for (const point of this.screenPoints) { + const delta = Math.abs(point.timeMs - timeMs); + if (delta < nearestDelta) { + nearestPoint = point; + nearestDelta = delta; + } + } + + return nearestPoint; + } + + renderLinkedHover(hoverPoint) { + this.crosshair.clear(); + + if (!hoverPoint) { + return; + } + + const x = clamp(hoverPoint.x, 0, this.bounds.width); + const y = clamp(hoverPoint.y, 0, this.bounds.height); + + this.crosshair.moveTo(x, 0); + this.crosshair.lineTo(x, this.bounds.height); + this.crosshair.moveTo(0, y); + this.crosshair.lineTo(this.bounds.width, y); + this.crosshair.stroke({ color: 0x8cb8ff, width: 1, alpha: 0.24 }); + this.crosshair.rect(x - 4, y - 4, 8, 8); + this.crosshair.stroke({ color: 0xffffff, width: 1.5, alpha: 0.95 }); + } + + ensureAxisLabelCount(count) { + while (this.axisLabels.length < count) { + const label = new Text({ + text: '', + style: { + fill: 0x90a0b7, + fontFamily: 'IBM Plex Mono, monospace', + fontSize: 10, + }, + }); + this.axisLabels.push(label); + this.axisLabelLayer.addChild(label); + } + + while (this.axisLabels.length > count) { + const label = this.axisLabels.pop(); + this.axisLabelLayer.removeChild(label); + label.destroy(); + } + } + + renderAxes({ padding, plotWidth, plotHeight, minTime, maxTime, minValue, maxValue, width }) { + const axisColor = 0x3e4c5f; + const tickColor = 0x4f627a; + const timeTickCount = 5; + const valueTickCount = 5; + const labels = []; + + this.axes.clear(); + this.axes.moveTo(padding.left, padding.top); + this.axes.lineTo(padding.left, padding.top + plotHeight); + this.axes.lineTo(padding.left + plotWidth, padding.top + plotHeight); + this.axes.stroke({ color: axisColor, width: 1, alpha: 1 }); + + for (let index = 0; index < timeTickCount; index += 1) { + const ratio = timeTickCount === 1 ? 0 : index / (timeTickCount - 1); + const y = padding.top + ratio * plotHeight; + const timeMs = minTime + ratio * (maxTime - minTime); + + this.axes.moveTo(padding.left - 8, y); + this.axes.lineTo(padding.left, y); + this.axes.stroke({ color: tickColor, width: 1, alpha: 1 }); + + labels.push({ + text: formatDuration(timeMs), + x: 14, + y: y - 7, + anchorX: 0, + }); + } + + for (let index = 0; index < valueTickCount; index += 1) { + const ratio = valueTickCount === 1 ? 0 : index / (valueTickCount - 1); + const x = padding.left + ratio * plotWidth; + const value = minValue + ratio * (maxValue - minValue); + + this.axes.moveTo(x, padding.top + plotHeight); + this.axes.lineTo(x, padding.top + plotHeight + 8); + this.axes.stroke({ color: tickColor, width: 1, alpha: 1 }); + + labels.push({ + text: formatValue(value), + x, + y: padding.top + plotHeight + 10, + anchorX: 0.5, + }); + } + + this.ensureAxisLabelCount(labels.length); + labels.forEach((config, index) => { + const label = this.axisLabels[index]; + label.text = config.text; + label.x = config.x; + label.y = config.y; + label.anchor.set(config.anchorX, 0); + }); + + this.axisTitleText.text = 'TIME'; + this.axisTitleText.x = 18; + this.axisTitleText.y = padding.top - 18; + this.axisTitleText.rotation = 0; + + this.axes.moveTo(padding.left + plotWidth, padding.top + plotHeight); + this.axes.lineTo(width - 14, padding.top + plotHeight); + this.axes.stroke({ color: 0x202a35, width: 1, alpha: 1 }); + } + + renderReadouts(state, width) { + if (!this.showReadouts) { + this.readoutBackground.clear(); + this.realTimeText.text = ''; + this.plotTimeText.text = ''; + return; + } + + const boxWidth = 168; + const boxHeight = 22; + const gap = 6; + const left = width - boxWidth - 18; + const top = 14; + + this.readoutBackground.clear(); + this.readoutBackground.rect(left, top, boxWidth, boxHeight); + this.readoutBackground.fill({ color: 0x10161d, alpha: 1 }); + this.readoutBackground.stroke({ color: 0x2f3c4d, width: 1, alpha: 1 }); + this.readoutBackground.rect(left, top + boxHeight + gap, boxWidth, boxHeight); + this.readoutBackground.fill({ color: 0x10161d, alpha: 1 }); + this.readoutBackground.stroke({ color: 0x2f3c4d, width: 1, alpha: 1 }); + + this.realTimeText.text = `REAL ${formatWallClock(state.time.realNowMs)}`; + this.realTimeText.x = left + 10; + this.realTimeText.y = top + 5; + + this.plotTimeText.text = `PLOT ${formatDuration(state.time.plotTimeMs)}`; + this.plotTimeText.x = left + 10; + this.plotTimeText.y = top + boxHeight + gap + 5; + } + + renderFrame(state, points) { + const width = this.bounds.width; + const height = this.bounds.height; + const padding = { top: 72, right: 28, bottom: 46, left: 88 }; + const plotWidth = Math.max(10, width - padding.left - padding.right); + const plotHeight = Math.max(10, height - padding.top - padding.bottom); + const minTime = state.time.plotTimeMs - state.plot.windowDurationMs; + const maxTime = Math.max(state.time.plotTimeMs, minTime + 1); + const { min: minValue, max: maxValue } = state.plot.valueRange; + const valueSpan = Math.max(0.001, maxValue - minValue); + + this.background.clear(); + roundRect( + this.background, + 0, + 0, + width, + height, + 6, + { color: 0x05070b, alpha: 1 }, + { color: 0x2c3b4d, width: 1 }, + ); + + this.grid.clear(); + if (state.plot.showGrid) { + const gridColor = 0x21344a; + for (let x = 0; x <= 6; x += 1) { + const px = padding.left + (plotWidth * x) / 6; + this.grid.moveTo(px, padding.top); + this.grid.lineTo(px, padding.top + plotHeight); + this.grid.stroke({ color: gridColor, width: 1, alpha: 0.85 }); + } + + for (let y = 0; y <= 8; y += 1) { + const py = padding.top + (plotHeight * y) / 8; + this.grid.moveTo(padding.left, py); + this.grid.lineTo(padding.left + plotWidth, py); + this.grid.stroke({ color: gridColor, width: 1, alpha: 0.85 }); + } + } + + this.renderAxes({ + padding, + plotWidth, + plotHeight, + minTime, + maxTime, + minValue, + maxValue, + width, + }); + + this.line.clear(); + this.points.clear(); + this.screenPoints = []; + + if (points.length > 0) { + points.forEach((point, index) => { + const x = padding.left + ((point.value - minValue) / valueSpan) * plotWidth; + const y = padding.top + ((point.timeMs - minTime) / (maxTime - minTime)) * plotHeight; + + this.screenPoints.push({ ...point, x, y }); + + if (index === 0) { + this.line.moveTo(x, y); + } else { + this.line.lineTo(x, y); + } + }); + + this.line.stroke({ + color: this.lineColor, + width: 2, + alpha: 0.96, + cap: 'square', + join: 'miter', + }); + + if (state.plot.showPoints) { + for (const point of this.screenPoints) { + this.points.rect(point.x - 2, point.y - 2, 4, 4); + this.points.fill({ color: this.pointColor, alpha: 0.92 }); + } + } + } + + this.titleText.text = this.panelTitle; + this.titleText.x = 20; + this.titleText.y = 14; + + this.subtitleText.text = this.panelSubtitle ?? `value → ${state.source.preset} · ${state.source.sampleRateHz} hz · time ↓`; + this.subtitleText.x = 20; + this.subtitleText.y = 36; + + this.renderReadouts(state, width); + } + + destroy() { + this.app.destroy(true, { children: true }); + } +} diff --git a/src/state.js b/src/state.js new file mode 100644 index 0000000..53d8279 --- /dev/null +++ b/src/state.js @@ -0,0 +1,420 @@ +/** + * StateManager - Centralized state management with Proxy-based reactivity + * + * Usage: + * state.time.speed = 2.0 // automatically emits events + * state.on('time.speed', (value) => console.log('Speed changed:', value)) + * state.on('time.*', (change) => console.log('Time domain changed:', change)) + * + * State Domains: + * - userPrefs: showGrid, showMetrics, theme, etc. + * - uiConfig: active panels, layout, dimensions + * - time: current time, speed, paused state, real elapsed time + * - rendering: graphs, renderer info + * - health: framerate, service connections, db access + * - dataInput: sources, structure, metadata + * - inputActions: keyboard/mouse/gamepad action mappings + */ + +// Simple EventEmitter implementation +class EventEmitter { + constructor() { + this.events = new Map(); + } + + on(event, callback) { + if (!this.events.has(event)) { + this.events.set(event, []); + } + this.events.get(event).push(callback); + + // Return unsubscribe function + return () => this.off(event, callback); + } + + off(event, callback) { + if (!this.events.has(event)) return; + const callbacks = this.events.get(event); + const index = callbacks.indexOf(callback); + if (index > -1) { + callbacks.splice(index, 1); + } + } + + emit(event, data) { + if (!this.events.has(event)) return; + this.events.get(event).forEach(callback => { + try { + callback(data); + } catch (e) { + console.error(`[State] Error in event handler for '${event}':`, e); + } + }); + } + + once(event, callback) { + const wrapper = (data) => { + callback(data); + this.off(event, wrapper); + }; + this.on(event, wrapper); + } + + clear() { + this.events.clear(); + } +} + +export class StateManager extends EventEmitter { + constructor() { + super(); + + // Internal state storage (not proxied) + this._state = { + userPrefs: { + showGrid: true, + showMetrics: true, + theme: 'dark', + rollingWindow: 60, + historyCapacity: 10000, + metricsUpdateInterval: 10, + }, + + uiConfig: { + activePanels: ['graph1', 'graph2'], + layout: 'horizontal-split', + canvasWidth: 0, + canvasHeight: 0, + }, + + time: { + current: 0, // Current plot time + realElapsed: 0, // Real time elapsed since start + speed: 1.0, // Time speed multiplier (0.1 to 5.0) + isPaused: false, // Pause state + startTimestamp: Date.now(), // Real timestamp when started + verticalScale: 1.0, // Vertical zoom for time history + }, + + rendering: { + rendererType: 'unknown', // 'webgpu' | 'webgl' | 'canvas' + frameCounter: 0, + // Note: graph instances are NOT stored here to avoid proxy wrapping + }, + + health: { + fps: 0, + updateMs: 0, + renderMs: 0, + vertexCount: 0, + lineCount: 0, + serviceConnections: {}, // e.g., { websocket: 'connected', mqtt: 'disconnected' } + }, + + dataInput: { + sources: [], // Array of data source configs + activeSource: null, // Currently active source + dataStructure: null, // Schema of incoming data + metadata: {}, // Additional metadata + }, + + inputActions: { + keyboardMap: new Map(), // Map of KeyboardEvent.code => action name + mouseMap: new Map(), // Map of mouse button => action name + actionHandlers: new Map(), // Map of action name => handler function + }, + }; + + // Track which domains should be persisted + this._persistedDomains = new Set(['userPrefs']); + + // Load persisted state + this._loadPersistedState(); + + // Create proxied state - this is what users interact with + this.state = this._createProxy(this._state, []); + } + + /** + * Create a reactive Proxy that emits events on property changes + * @param {Object} target - The object to proxy + * @param {Array} path - Current property path (e.g., ['time', 'speed']) + * @private + */ + _createProxy(target, path) { + // Don't proxy non-objects or special objects like Map/Set + if (typeof target !== 'object' || target === null) { + return target; + } + + // Don't proxy Maps and Sets - they need special handling + if (target instanceof Map || target instanceof Set) { + return target; + } + + const self = this; + + return new Proxy(target, { + get(obj, prop) { + const value = obj[prop]; + + // Return primitives and functions as-is + if (typeof value !== 'object' || value === null) { + return value; + } + + // Return nested objects as proxies + return self._createProxy(value, [...path, prop]); + }, + + set(obj, prop, value) { + const oldValue = obj[prop]; + + // Only emit if value actually changed + if (oldValue === value) { + return true; + } + + obj[prop] = value; + + // Build event path + const fullPath = [...path, prop]; + const pathString = fullPath.join('.'); + const domain = fullPath[0]; + + // Emit specific property change: "time.speed" + self.emit(pathString, { + path: fullPath, + value: value, + oldValue: oldValue, + }); + + // Emit domain wildcard: "time.*" + if (domain) { + self.emit(`${domain}.*`, { + path: fullPath, + property: prop, + value: value, + oldValue: oldValue, + }); + } + + // Emit global wildcard: "*" + self.emit('*', { + path: fullPath, + value: value, + oldValue: oldValue, + }); + + // Auto-persist certain domains + if (self._persistedDomains.has(domain)) { + self._persistDomain(domain); + } + + return true; + } + }); + } + + // ========================================================================= + // Persistence + // ========================================================================= + + _persistDomain(domain) { + try { + const data = this._state[domain]; + // Convert Maps to objects for JSON serialization + const serializable = this._makeSerializable(data); + localStorage.setItem(`timeplot-${domain}`, JSON.stringify(serializable)); + } catch (e) { + console.warn(`[State] Failed to persist ${domain}:`, e); + } + } + + _loadPersistedState() { + this._persistedDomains.forEach(domain => { + try { + const saved = localStorage.getItem(`timeplot-${domain}`); + if (saved) { + const data = JSON.parse(saved); + // Deep merge to preserve defaults for new properties + this._state[domain] = this._deepMerge(this._state[domain], data); + } + } catch (e) { + console.warn(`[State] Failed to load ${domain}:`, e); + } + }); + } + + _makeSerializable(obj) { + if (obj instanceof Map) { + return Object.fromEntries(obj); + } + if (obj instanceof Set) { + return Array.from(obj); + } + if (typeof obj === 'object' && obj !== null) { + const result = {}; + for (const [key, value] of Object.entries(obj)) { + result[key] = this._makeSerializable(value); + } + return result; + } + return obj; + } + + _deepMerge(target, source) { + const result = { ...target }; + for (const key in source) { + if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) { + result[key] = this._deepMerge(target[key] || {}, source[key]); + } else { + result[key] = source[key]; + } + } + return result; + } + + // ========================================================================= + // Convenience Methods + // ========================================================================= + + /** + * Toggle a boolean preference + */ + togglePref(key) { + const current = this.state.userPrefs[key]; + if (typeof current === 'boolean') { + this.state.userPrefs[key] = !current; + } + } + + /** + * Pause/resume time + */ + togglePause() { + this.state.time.isPaused = !this.state.time.isPaused; + } + + /** + * Set time speed (clamped 0.1 to 5.0) + */ + setTimeSpeed(speed) { + this.state.time.speed = Math.max(0.1, Math.min(5.0, speed)); + } + + /** + * Increment time (respects pause and speed) + */ + incrementTime(delta) { + if (this.state.time.isPaused) return; + this.state.time.current += delta * this.state.time.speed; + } + + /** + * Update real elapsed time + */ + updateRealElapsed() { + const elapsed = (Date.now() - this.state.time.startTimestamp) / 1000; + this.state.time.realElapsed = elapsed; + } + + // ========================================================================= + // Input Actions System + // ========================================================================= + + /** + * Register an input action handler + * @param {string} actionName - Name of the action (e.g., 'toggleGrid', 'pause') + * @param {Function} handler - Handler function to call + */ + registerAction(actionName, handler) { + this.state.inputActions.actionHandlers.set(actionName, handler); + } + + /** + * Map a keyboard key to an action + * @param {string} code - KeyboardEvent.code (e.g., 'KeyG', 'Space') + * @param {string} actionName - Action to trigger + */ + mapKey(code, actionName) { + this.state.inputActions.keyboardMap.set(code, actionName); + } + + /** + * Map a mouse button to an action + * @param {number} button - Mouse button number (0=left, 1=middle, 2=right) + * @param {string} actionName - Action to trigger + */ + mapMouseButton(button, actionName) { + this.state.inputActions.mouseMap.set(button, actionName); + } + + /** + * Execute an action by name + */ + executeAction(actionName, event) { + const handler = this.state.inputActions.actionHandlers.get(actionName); + if (handler) { + handler(event); + } else { + console.warn(`[State] No handler registered for action: ${actionName}`); + } + } + + /** + * Handle keyboard event through action system + */ + handleKeyboardEvent(event) { + const actionName = this.state.inputActions.keyboardMap.get(event.code); + if (actionName) { + this.executeAction(actionName, event); + return true; + } + return false; + } + + /** + * Handle mouse button event through action system + */ + handleMouseButtonEvent(event) { + const actionName = this.state.inputActions.mouseMap.get(event.button); + if (actionName) { + this.executeAction(actionName, event); + return true; + } + return false; + } + + // ========================================================================= + // Data Sources + // ========================================================================= + + addDataSource(source) { + this.state.dataInput.sources.push(source); + } + + removeDataSource(sourceId) { + const sources = this.state.dataInput.sources; + const index = sources.findIndex(s => s.id === sourceId); + if (index > -1) { + sources.splice(index, 1); + } + } + + setActiveDataSource(sourceId) { + this.state.dataInput.activeSource = sourceId; + } + + // ========================================================================= + // Debugging + // ========================================================================= + + dump() { + console.log('[State] Current state:', JSON.parse(JSON.stringify(this._state))); + } + + debugEvents() { + console.log('[State] Registered events:', Array.from(this.events.keys())); + } +} diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..6b0477f --- /dev/null +++ b/src/styles.css @@ -0,0 +1,401 @@ +:root { + color-scheme: dark; + --bg: #0a0c10; + --bg-alt: #0f1319; + --surface: #11161d; + --surface-strong: #0d1117; + --surface-raised: #171d26; + --border: #28313d; + --border-strong: #394657; + --text: #edf2f7; + --muted: #97a3b4; + --accent: #9fc7ff; + --accent-strong: #d8e8ff; + --shadow: none; + font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + width: 100%; + height: 100%; + margin: 0; +} + +body { + background: + linear-gradient(180deg, #080a0d 0%, #0d1015 100%); + color: var(--text); + overflow: hidden; +} + +button, +input, +select { + font: inherit; +} + +.timeplot-shell { + display: grid; + grid-template-columns: minmax(0, 1fr) 340px; + grid-template-rows: auto minmax(0, 1fr); + width: 100%; + height: 100%; + gap: 10px; + padding: 10px; +} + +.timeplot-topbar { + grid-column: 1 / -1; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 12px 14px; + border: 1px solid var(--border-strong); + background: var(--surface); + border-radius: 4px; + box-shadow: var(--shadow); +} + +.timeplot-brand { + display: flex; + flex-direction: column; + gap: 2px; +} + +.timeplot-title { + margin: 0; + font-size: 1rem; + letter-spacing: 0.08em; + text-transform: uppercase; + font-weight: 700; +} + +.timeplot-subtitle { + color: var(--muted); + font-size: 0.78rem; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.timeplot-toolbar { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.control-group { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + background: var(--surface-raised); + border: 1px solid var(--border); + border-radius: 3px; +} + +.control-group label, +.control-group span { + color: var(--muted); + font-size: 0.74rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.control-group input[type='range'] { + width: 118px; +} + +.control-group input[type='range'] { + accent-color: var(--accent); +} + +.control-button, +.panel-toggle { + color: var(--text); + background: var(--surface); + border: 1px solid var(--border-strong); + border-radius: 2px; + padding: 7px 11px; + cursor: pointer; + transition: border-color 120ms ease, background 120ms ease, color 120ms ease; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.72rem; + line-height: 1; +} + +.control-button:hover, +.panel-toggle:hover { + border-color: var(--accent); + color: var(--accent-strong); +} + +.control-button[data-active='true'], +.panel-toggle[data-active='true'] { + background: #1a2230; + border-color: var(--accent); + color: var(--accent-strong); +} + +.timeplot-viewport { + position: relative; + min-height: 0; + border-radius: 4px; + overflow: hidden; + border: 1px solid var(--border-strong); + background: #06080b; + box-shadow: var(--shadow); + padding: 10px; +} + +.timeplot-plot-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 10px; + width: 100%; + height: 100%; + min-height: 0; +} + +.timeplot-plot-panel { + position: relative; + min-width: 0; + min-height: 0; + border: 1px solid var(--border); + background: #070a0d; +} + +.timeplot-canvas-host { + width: 100%; + height: 100%; +} + +.timeplot-sidebar { + display: flex; + flex-direction: column; + gap: 10px; + min-height: 0; + overflow-y: auto; + padding-right: 2px; +} + +.panel { + border: 1px solid var(--border-strong); + background: var(--surface-strong); + border-radius: 4px; + padding: 14px; +} + +.panel[hidden] { + display: none; +} + +.panel h2 { + margin: 0 0 12px; + font-size: 0.8rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.panel-subsection + .panel-subsection { + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid var(--border); +} + +.panel-section-title { + margin-bottom: 10px; + color: var(--accent-strong); + font-size: 0.72rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.kv-list { + display: grid; + grid-template-columns: auto 1fr; + gap: 10px 12px; + align-items: center; + margin: 0; +} + +.kv-list dt { + color: var(--muted); + font-size: 0.73rem; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.kv-list dd { + margin: 0; + text-align: right; + font-variant-numeric: tabular-nums; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; +} + +.field-grid { + display: grid; + gap: 12px; +} + +.field-grid label { + display: grid; + gap: 6px; + color: var(--muted); + font-size: 0.74rem; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.field-grid[data-source-mode][hidden] { + display: none; +} + +.source-meta { + min-height: 20px; + color: var(--muted); + font-size: 0.76rem; + line-height: 1.4; +} + +.source-meta-error { + color: #ff9d9d; +} + +.source-meta-status { + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.source-meta-status-connected { + color: #99e2b4; +} + +.source-meta-status-connecting { + color: #ffd27f; +} + +.source-meta-status-disconnected, +.source-meta-status-idle { + color: var(--muted); +} + +.source-meta-status-error { + color: #ff9d9d; +} + +.field-grid input, +.field-grid select { + width: 100%; + padding: 9px 10px; + border-radius: 2px; + border: 1px solid var(--border); + background: var(--surface-raised); + color: var(--text); +} + +.field-grid input:focus, +.field-grid select:focus { + outline: none; + border-color: var(--accent); +} + +.panel-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + color: var(--muted); + font-size: 0.74rem; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.panel-row + .panel-row { + margin-top: 10px; +} + +.panel-row input[type='checkbox'] { + inline-size: 16px; + block-size: 16px; + accent-color: var(--accent); +} + +.muted { + color: var(--muted); +} + +.help-list { + display: grid; + gap: 8px; + margin: 0; + padding-left: 18px; + color: var(--muted); + font-size: 0.82rem; +} + +.timeplot-tooltip { + position: absolute; + min-width: 180px; + padding: 10px 12px; + border-radius: 3px; + border: 1px solid var(--border-strong); + background: #0d1218; + box-shadow: var(--shadow); + pointer-events: none; + transform: translate(12px, -50%); + z-index: 10; +} + +.timeplot-tooltip[hidden] { + display: none; +} + +.timeplot-tooltip-title { + margin-bottom: 8px; + font-size: 0.72rem; + color: var(--accent-strong); + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.timeplot-tooltip-row { + display: flex; + justify-content: space-between; + gap: 16px; + font-size: 0.78rem; +} + +.timeplot-tooltip-row + .timeplot-tooltip-row { + margin-top: 4px; +} + +.timeplot-empty { + color: var(--muted); + font-size: 0.85rem; +} + +@media (max-width: 1100px) { + .timeplot-shell { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto minmax(360px, 1fr) auto; + } + + .timeplot-plot-grid { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: repeat(2, minmax(260px, 1fr)); + } + + .timeplot-sidebar { + overflow: visible; + } +} diff --git a/src/template-for-standard-site.js b/src/template-for-standard-site.js new file mode 100644 index 0000000..54aacc7 --- /dev/null +++ b/src/template-for-standard-site.js @@ -0,0 +1,75 @@ +//import { setupRenderSystem } from './render.js'; + +let ENVURL = "" //remote server from which to grab env +let env = {}; +let cfg = {}; //the user config +let dom = { + input: {}, + label: {}, + box: {}, //an info-containing box + icon: {}, + info: {} +}; + + +//APP START HERE +$(document).ready(async function() { + console.log('asdf'); + //the core loop of the client application + // 1. setup relationship with DOM and grab references to its elements + log('init DOM'); + await initDOM(); + + log('init cfg'); + await initCfg(); + + log('get env vars'); + await getServerEnvVars(); + + log('init services'); + await initServices(); + + //setupRenderSystem(); + + +}); + +//gets user config from local storage if there is any +function initCfg(){ + let localCfg = localStorage.getItem('cfg'); + if (localCfg) { + try { + cfg = JSON.parse(localCfg); + } catch (e) { + cfg = {}; + } + } else { + + } +} + +async function getServerEnvVars(){ + await axios.get(`${ENVURL}`).then((res)=>{ + env = res.data; + //log(env); + }).catch((err)=>{ + //log(err); + }); + log('') +} + +function initServices(){ + //connect to websocket server + //grab endpoints from cfg +} + +function initDOM(){ + dom.body = $('body')[0]; +} + +function log(msg, lvl=1){ + if (dom.debugInfo){ + dom.debugInfo.innerHTML = msg; //TODO running log + timestamp + } + console.log(msg); +} \ No newline at end of file diff --git a/src/test-data-generators.js b/src/test-data-generators.js new file mode 100644 index 0000000..02bc0ad --- /dev/null +++ b/src/test-data-generators.js @@ -0,0 +1,530 @@ +/** + * Test Data Generators - Classes for generating fake/test data patterns + * + * These generators produce various types of synthetic data for testing + * and visualizing the waterfall graphs with realistic patterns. + */ + +/** + * Base class for all data generators + */ +class DataGenerator { + constructor(config = {}) { + this.sampleRate = config.sampleRate || 100; // Samples per second + this.amplitude = config.amplitude || 1.0; + this.offset = config.offset || 0.0; + this.time = 0; + } + + /** + * Generate a single sample at the current time + * @returns {number} The generated value + */ + sample() { + throw new Error('sample() must be implemented by subclass'); + } + + /** + * Generate an array of samples + * @param {number} count - Number of samples to generate + * @returns {Array} Array of generated values + */ + generateSamples(count) { + const samples = []; + for (let i = 0; i < count; i++) { + samples.push(this.sample()); + this.time += 1 / this.sampleRate; + } + return samples; + } + + /** + * Generate a line of points for waterfall display + * @param {number} pointCount - Number of points in the line + * @param {number} width - Width of the display area + * @returns {Array<{x: number, y: number}>} Array of points + */ + generateLine(pointCount, width) { + const points = []; + const samples = this.generateSamples(pointCount); + + for (let i = 0; i < pointCount; i++) { + const x = (i / pointCount) * width; + const y = samples[i] * this.amplitude + this.offset; + points.push({ x, y }); + } + + return points; + } + + reset() { + this.time = 0; + } +} + +/** + * Sine Wave Generator - Classic sinusoidal wave + */ +export class SineWaveGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.frequency = config.frequency || 1.0; // Hz + this.phase = config.phase || 0.0; // Radians + } + + sample() { + const value = Math.sin(2 * Math.PI * this.frequency * this.time + this.phase); + return value; + } +} + +/** + * Square Wave Generator - Digital-style square wave + */ +export class SquareWaveGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.frequency = config.frequency || 1.0; + this.dutyCycle = config.dutyCycle || 0.5; // 0.0 to 1.0 + } + + sample() { + const period = 1 / this.frequency; + const phase = (this.time % period) / period; + return phase < this.dutyCycle ? 1.0 : -1.0; + } +} + +/** + * Sawtooth Wave Generator - Linear ramp wave + */ +export class SawtoothWaveGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.frequency = config.frequency || 1.0; + } + + sample() { + const period = 1 / this.frequency; + const phase = (this.time % period) / period; + return 2 * phase - 1; // -1 to 1 + } +} + +/** + * Triangle Wave Generator - Linear up/down wave + */ +export class TriangleWaveGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.frequency = config.frequency || 1.0; + } + + sample() { + const period = 1 / this.frequency; + const phase = (this.time % period) / period; + return phase < 0.5 + ? 4 * phase - 1 + : 3 - 4 * phase; + } +} + +/** + * White Noise Generator - Random noise + */ +export class WhiteNoiseGenerator extends DataGenerator { + sample() { + return Math.random() * 2 - 1; // -1 to 1 + } +} + +/** + * Pink Noise Generator - 1/f noise (more realistic than white noise) + */ +export class PinkNoiseGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + // Paul Kellet's refined method + this.b0 = 0; + this.b1 = 0; + this.b2 = 0; + this.b3 = 0; + this.b4 = 0; + this.b5 = 0; + this.b6 = 0; + } + + sample() { + const white = Math.random() * 2 - 1; + this.b0 = 0.99886 * this.b0 + white * 0.0555179; + this.b1 = 0.99332 * this.b1 + white * 0.0750759; + this.b2 = 0.96900 * this.b2 + white * 0.1538520; + this.b3 = 0.86650 * this.b3 + white * 0.3104856; + this.b4 = 0.55000 * this.b4 + white * 0.5329522; + this.b5 = -0.7616 * this.b5 - white * 0.0168980; + const pink = this.b0 + this.b1 + this.b2 + this.b3 + this.b4 + this.b5 + this.b6 + white * 0.5362; + this.b6 = white * 0.115926; + return pink * 0.11; // Normalize + } +} + +/** + * Perlin Noise Generator - Smooth, continuous noise + */ +export class PerlinNoiseGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.frequency = config.frequency || 1.0; + this.octaves = config.octaves || 4; + this.persistence = config.persistence || 0.5; + } + + // Simple 1D Perlin-like noise + noise(x) { + const i = Math.floor(x); + const f = x - i; + + // Fade curve + const u = f * f * (3 - 2 * f); + + // Hash function for pseudo-random gradients + const hash = (n) => { + n = (n << 13) ^ n; + return (1.0 - ((n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0); + }; + + return (1 - u) * hash(i) + u * hash(i + 1); + } + + sample() { + let value = 0; + let amplitude = 1; + let frequency = this.frequency; + let maxValue = 0; + + for (let i = 0; i < this.octaves; i++) { + value += this.noise(this.time * frequency) * amplitude; + maxValue += amplitude; + amplitude *= this.persistence; + frequency *= 2; + } + + return value / maxValue; + } +} + +/** + * Pulse/Spike Generator - Random spikes/pulses + */ +export class PulseGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.pulseRate = config.pulseRate || 0.05; // Probability per sample + this.pulseWidth = config.pulseWidth || 0.01; // Duration in seconds + this.pulseAmplitude = config.pulseAmplitude || 1.0; + this.currentPulse = null; + } + + sample() { + // Check if we're in a pulse + if (this.currentPulse) { + if (this.time >= this.currentPulse.endTime) { + this.currentPulse = null; + } else { + return this.pulseAmplitude; + } + } + + // Random chance to start new pulse + if (Math.random() < this.pulseRate) { + this.currentPulse = { + startTime: this.time, + endTime: this.time + this.pulseWidth, + }; + return this.pulseAmplitude; + } + + return 0; + } +} + +/** + * Burst Generator - Bursts of activity with quiet periods + */ +export class BurstGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.burstDuration = config.burstDuration || 1.0; // Seconds + this.quietDuration = config.quietDuration || 2.0; // Seconds + this.burstFrequency = config.burstFrequency || 5.0; // Hz during burst + this.currentState = 'quiet'; + this.stateStartTime = 0; + } + + sample() { + const elapsed = this.time - this.stateStartTime; + + // State transitions + if (this.currentState === 'quiet' && elapsed >= this.quietDuration) { + this.currentState = 'burst'; + this.stateStartTime = this.time; + } else if (this.currentState === 'burst' && elapsed >= this.burstDuration) { + this.currentState = 'quiet'; + this.stateStartTime = this.time; + } + + // Generate value based on state + if (this.currentState === 'burst') { + return Math.sin(2 * Math.PI * this.burstFrequency * this.time); + } else { + return 0; + } + } +} + +/** + * Chirp Generator - Frequency sweep signal + */ +export class ChirpGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.startFreq = config.startFreq || 0.5; // Hz + this.endFreq = config.endFreq || 10.0; // Hz + this.duration = config.duration || 5.0; // Seconds + } + + sample() { + const t = this.time % this.duration; + const progress = t / this.duration; + const freq = this.startFreq + (this.endFreq - this.startFreq) * progress; + return Math.sin(2 * Math.PI * freq * t); + } +} + +/** + * Composite Generator - Combine multiple generators + */ +export class CompositeGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.generators = config.generators || []; + this.weights = config.weights || this.generators.map(() => 1.0); + } + + sample() { + let sum = 0; + let weightSum = 0; + + for (let i = 0; i < this.generators.length; i++) { + sum += this.generators[i].sample() * this.weights[i]; + weightSum += this.weights[i]; + } + + return weightSum > 0 ? sum / weightSum : 0; + } + + generateSamples(count) { + const samples = []; + for (let i = 0; i < count; i++) { + samples.push(this.sample()); + // Advance all child generators + this.generators.forEach(gen => gen.time += 1 / gen.sampleRate); + } + return samples; + } +} + +/** + * FM (Frequency Modulation) Generator - One signal modulates another + */ +export class FMGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.carrierFreq = config.carrierFreq || 5.0; // Hz + this.modulatorFreq = config.modulatorFreq || 0.5; // Hz + this.modulationIndex = config.modulationIndex || 2.0; + } + + sample() { + const modulator = Math.sin(2 * Math.PI * this.modulatorFreq * this.time); + const instantFreq = this.carrierFreq + this.modulationIndex * modulator; + return Math.sin(2 * Math.PI * instantFreq * this.time); + } +} + +/** + * Exponential Decay Generator - Exponentially decaying signal + */ +export class ExponentialDecayGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.decayRate = config.decayRate || 1.0; // 1/seconds + this.oscillationFreq = config.oscillationFreq || 5.0; // Hz + } + + sample() { + const envelope = Math.exp(-this.decayRate * this.time); + const oscillation = Math.sin(2 * Math.PI * this.oscillationFreq * this.time); + return envelope * oscillation; + } +} + +/** + * Step Function Generator - Random walk / brownian motion + */ +export class RandomWalkGenerator extends DataGenerator { + constructor(config = {}) { + super(config); + this.stepSize = config.stepSize || 0.1; + this.currentValue = 0; + this.bounds = config.bounds || { min: -5, max: 5 }; + } + + sample() { + // Random step + const step = (Math.random() - 0.5) * this.stepSize; + this.currentValue += step; + + // Apply bounds + this.currentValue = Math.max(this.bounds.min, Math.min(this.bounds.max, this.currentValue)); + + return this.currentValue; + } +} + +// ============================================================================ +// Example Usage and Presets +// ============================================================================ + +/** + * Factory function to create common test scenarios + */ +export class TestDataFactory { + static createSimpleSine(amplitude = 30) { + return new SineWaveGenerator({ + frequency: 2.0, + amplitude: amplitude, + sampleRate: 100, + }); + } + + static createNoisySine(amplitude = 30) { + const sine = new SineWaveGenerator({ + frequency: 2.0, + amplitude: amplitude * 0.8, + sampleRate: 100, + }); + + const noise = new WhiteNoiseGenerator({ + amplitude: amplitude * 0.2, + sampleRate: 100, + }); + + return new CompositeGenerator({ + generators: [sine, noise], + weights: [1.0, 1.0], + }); + } + + static createComplexPattern(amplitude = 30) { + const low = new SineWaveGenerator({ + frequency: 0.5, + amplitude: amplitude * 0.4, + sampleRate: 100, + }); + + const mid = new SineWaveGenerator({ + frequency: 3.0, + amplitude: amplitude * 0.3, + sampleRate: 100, + }); + + const high = new SineWaveGenerator({ + frequency: 8.0, + amplitude: amplitude * 0.2, + sampleRate: 100, + }); + + const noise = new PinkNoiseGenerator({ + amplitude: amplitude * 0.1, + sampleRate: 100, + }); + + return new CompositeGenerator({ + generators: [low, mid, high, noise], + weights: [1.0, 1.0, 1.0, 1.0], + }); + } + + static createBurstySignal(amplitude = 30) { + return new BurstGenerator({ + amplitude: amplitude, + burstDuration: 0.5, + quietDuration: 1.5, + burstFrequency: 10.0, + sampleRate: 100, + }); + } + + static createSmoothNoise(amplitude = 30) { + return new PerlinNoiseGenerator({ + amplitude: amplitude, + frequency: 2.0, + octaves: 3, + persistence: 0.5, + sampleRate: 100, + }); + } + + static createFrequencySweep(amplitude = 30) { + return new ChirpGenerator({ + amplitude: amplitude, + startFreq: 0.5, + endFreq: 10.0, + duration: 3.0, + sampleRate: 100, + }); + } + + static createModulatedSignal(amplitude = 30) { + return new FMGenerator({ + amplitude: amplitude, + carrierFreq: 5.0, + modulatorFreq: 0.3, + modulationIndex: 3.0, + sampleRate: 100, + }); + } + + static createRandomWalk(amplitude = 30) { + return new RandomWalkGenerator({ + stepSize: 0.5, + bounds: { min: -amplitude, max: amplitude }, + sampleRate: 100, + }); + } +} + +/** + * Example: How to use with WaterfallGraph + * + * // Create a generator + * const generator = TestDataFactory.createComplexPattern(30); + * + * // In your graph's addLine method: + * addLine(time, graphIdx) { + * const line = { + * points: generator.generateLine(this.pointsPerLine, this.width), + * yOffset: 0, + * color: this.generateColor(time), + * }; + * this.lines.push(line); + * } + * + * // Or generate custom samples: + * const samples = generator.generateSamples(100); + * const points = samples.map((y, i) => ({ + * x: (i / samples.length) * width, + * y: y + * })); + */ diff --git a/src/timeseries-plot.js b/src/timeseries-plot.js new file mode 100644 index 0000000..e35a704 --- /dev/null +++ b/src/timeseries-plot.js @@ -0,0 +1,277 @@ +import { Container, Graphics, Text } from 'pixi.js'; + +/** + * TimeSeriesPlot - Pure visualization component for time-series data + * + * This class is responsible ONLY for displaying data, not generating it. + * It receives data points from external sources and renders them as a + * scrolling waterfall display. + * + * Architecture: + * - TimeSeriesPlot: Displays data (this file) + * - DataSource: Generates/provides data (data-sources.js) + * - Connection: Links sources to plots + */ +export class TimeSeriesPlot { + constructor(config) { + this.x = config.x || 0; + this.y = config.y || 0; + this.width = config.width || 800; + this.height = config.height || 600; + this.title = config.title || 'Time Series'; + this.baseColor = config.color || 0xff6666; + + // Container for all graphics + this.container = new Container(); + this.container.x = this.x; + this.container.y = this.y; + + // Graphics layers (order matters for rendering) + this.gridGraphics = new Graphics(); + this.linesGraphics = new Graphics(); + this.borderGraphics = new Graphics(); + + this.container.addChild(this.gridGraphics); + this.container.addChild(this.linesGraphics); + this.container.addChild(this.borderGraphics); + + // Title + this.titleText = new Text({ + text: this.title, + style: { + fontFamily: 'Arial', + fontSize: 18, + fill: 0xeeeeee, + } + }); + this.titleText.x = 10; + this.titleText.y = 10; + this.container.addChild(this.titleText); + + // Display state + this.lines = []; // Array of {points, yOffset, color, metadata} + this.maxLines = config.maxLines || 100; + this.showGrid = config.showGrid !== false; + + // Scrolling and scaling + this.scrollSpeed = config.scrollSpeed || 1.0; + this.verticalScale = config.verticalScale || 1.0; + + // Initial draw + this.draw(); + } + + // ======================================================================== + // Data Input API - This is how external sources send data to the plot + // ======================================================================== + + /** + * Add a new line of data to the plot + * @param {Array<{x: number, y: number}>} points - Array of points + * @param {Object} metadata - Optional metadata (color, timestamp, etc.) + */ + addLine(points, metadata = {}) { + const line = { + points: points, + yOffset: 0, + color: metadata.color || this.generateColor(Date.now() / 1000), + timestamp: metadata.timestamp || Date.now(), + metadata: metadata, + }; + + this.lines.push(line); + + // Limit number of lines + if (this.lines.length > this.maxLines) { + this.lines.shift(); + } + } + + /** + * Add a single data point (will be buffered into a line) + * This is useful for streaming real-time data + * @param {number} timestamp - Time of the data point + * @param {number} value - Value at this time + */ + addDataPoint(timestamp, value) { + // For now, this creates a single-point line + // In a more sophisticated version, this could buffer points + // until a full line is ready + const point = { x: this.width / 2, y: value }; + this.addLine([point], { timestamp }); + } + + /** + * Clear all data from the plot + */ + clearData() { + this.lines = []; + this.drawLines(); + } + + // ======================================================================== + // Update and Rendering + // ======================================================================== + + /** + * Update the plot - called each frame + * This handles scrolling and cleanup, but NOT data generation + */ + update() { + // Scroll existing lines down + this.scrollLines(); + + // Remove off-screen lines + this.lines = this.lines.filter(line => { + const scaledOffset = line.yOffset * this.verticalScale; + return scaledOffset < this.height + 50; + }); + + // Redraw + this.drawLines(); + } + + scrollLines() { + this.lines.forEach(line => { + line.yOffset += this.scrollSpeed; + }); + } + + draw() { + this.drawBorder(); + this.drawGrid(); + this.drawLines(); + } + + drawBorder() { + this.borderGraphics.clear(); + this.borderGraphics.rect(0, 0, this.width, this.height); + this.borderGraphics.stroke({ width: 2, color: 0x606070 }); + } + + drawGrid() { + this.gridGraphics.clear(); + + if (!this.showGrid) return; + + this.gridGraphics.alpha = 0.3; + + const divisions = 10; + const color = 0x4a7a9a; + + // Vertical lines + for (let i = 0; i <= divisions; i++) { + const x = (i / divisions) * this.width; + this.gridGraphics.moveTo(x, 0); + this.gridGraphics.lineTo(x, this.height); + this.gridGraphics.stroke({ width: 1, color }); + } + + // Horizontal lines + for (let i = 0; i <= divisions; i++) { + const y = (i / divisions) * this.height; + this.gridGraphics.moveTo(0, y); + this.gridGraphics.lineTo(this.width, y); + this.gridGraphics.stroke({ width: 1, color }); + } + } + + drawLines() { + this.linesGraphics.clear(); + + for (const line of this.lines) { + if (line.points.length < 2) continue; + + // Apply vertical scale to y positions + const scaledYOffset = line.yOffset * this.verticalScale; + + // Start path + const firstPoint = line.points[0]; + this.linesGraphics.moveTo(firstPoint.x, firstPoint.y + scaledYOffset); + + // Draw line strip + for (let i = 1; i < line.points.length; i++) { + const point = line.points[i]; + this.linesGraphics.lineTo(point.x, point.y + scaledYOffset); + } + + this.linesGraphics.stroke({ width: 2, color: line.color }); + } + } + + generateColor(time) { + // Cycle through colors based on time + const hue = (time * 0.1) % 1.0; + const r = Math.floor(Math.abs(Math.sin(hue * Math.PI * 2)) * 255); + const g = Math.floor(Math.abs(Math.sin((hue + 0.33) * Math.PI * 2)) * 255); + const b = Math.floor(Math.abs(Math.sin((hue + 0.66) * Math.PI * 2)) * 255); + + return (r << 16) | (g << 8) | b; + } + + // ======================================================================== + // Configuration and Control + // ======================================================================== + + setGridVisible(visible) { + this.showGrid = visible; + this.drawGrid(); + } + + setScrollSpeed(speed) { + this.scrollSpeed = Math.max(0.1, Math.min(10.0, speed)); + } + + setVerticalScale(scale) { + this.verticalScale = Math.max(0.2, Math.min(3.0, scale)); + } + + setTitle(title) { + this.title = title; + this.titleText.text = title; + } + + resize(x, y, width, height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + + this.container.x = x; + this.container.y = y; + + this.draw(); + } + + // ======================================================================== + // Statistics and Debugging + // ======================================================================== + + getVertexCount() { + return this.lines.reduce((sum, line) => sum + line.points.length, 0); + } + + getLineCount() { + return this.lines.length; + } + + getOldestTimestamp() { + if (this.lines.length === 0) return null; + return Math.min(...this.lines.map(l => l.timestamp)); + } + + getNewestTimestamp() { + if (this.lines.length === 0) return null; + return Math.max(...this.lines.map(l => l.timestamp)); + } + + getStats() { + return { + lineCount: this.getLineCount(), + vertexCount: this.getVertexCount(), + oldestTimestamp: this.getOldestTimestamp(), + newestTimestamp: this.getNewestTimestamp(), + timeSpan: this.getNewestTimestamp() - this.getOldestTimestamp(), + }; + } +} diff --git a/src/ui/panel-manager.js b/src/ui/panel-manager.js new file mode 100644 index 0000000..ad29697 --- /dev/null +++ b/src/ui/panel-manager.js @@ -0,0 +1,542 @@ +import { formatDuration, formatValue, formatWallClock } from '../utils-format.js'; + +function createElement(tagName, className, textContent) { + const element = document.createElement(tagName); + if (className) { + element.className = className; + } + if (textContent) { + element.textContent = textContent; + } + return element; +} + +function setToggleState(element, active) { + element.dataset.active = String(active); +} + +function readControlValue(element) { + if (element.tagName === 'SELECT') { + return element.value; + } + + if (element instanceof HTMLInputElement) { + if (element.type === 'checkbox') { + return element.checked; + } + + if (element.type === 'number' || element.type === 'range') { + return Number(element.value); + } + + return element.value; + } + + return element.value; +} + +function syncControlValue(element, value) { + if (!element || document.activeElement === element) { + return; + } + + if (element instanceof HTMLInputElement && element.type === 'checkbox') { + element.checked = Boolean(value); + return; + } + + element.value = String(value ?? ''); +} + +export class PanelManager { + constructor({ root, store, actions }) { + this.root = root; + this.store = store; + this.actions = actions; + this.elements = {}; + } + + mount() { + const shell = createElement('div', 'timeplot-shell'); + const topbar = createElement('header', 'timeplot-topbar'); + const viewport = createElement('section', 'timeplot-viewport'); + const plotGrid = createElement('div', 'timeplot-plot-grid'); + const primaryPlotPanel = createElement('section', 'timeplot-plot-panel'); + const secondaryPlotPanel = createElement('section', 'timeplot-plot-panel'); + const primaryCanvasHost = createElement('div', 'timeplot-canvas-host'); + const secondaryCanvasHost = createElement('div', 'timeplot-canvas-host'); + const sidebar = createElement('aside', 'timeplot-sidebar'); + const primaryTooltip = createElement('div', 'timeplot-tooltip'); + const secondaryTooltip = createElement('div', 'timeplot-tooltip'); + primaryTooltip.hidden = true; + secondaryTooltip.hidden = true; + + const brand = createElement('div', 'timeplot-brand'); + const title = createElement('h1', 'timeplot-title', 'TimePlot'); + const subtitle = createElement('div', 'timeplot-subtitle', 'Dual synchronized signal monitor'); + brand.append(title, subtitle); + + const toolbar = createElement('div', 'timeplot-toolbar'); + toolbar.append( + this.createTransportControls(), + this.createPanelToggles(), + ); + + topbar.append(brand, toolbar); + primaryPlotPanel.append(primaryCanvasHost, primaryTooltip); + secondaryPlotPanel.append(secondaryCanvasHost, secondaryTooltip); + plotGrid.append(primaryPlotPanel, secondaryPlotPanel); + viewport.append(plotGrid); + shell.append(topbar, viewport, sidebar); + this.root.replaceChildren(shell); + + this.elements = { + ...this.elements, + shell, + topbar, + viewport, + plotGrid, + primaryPlotPanel, + secondaryPlotPanel, + primaryCanvasHost, + secondaryCanvasHost, + sidebar, + primaryTooltip, + secondaryTooltip, + title, + subtitle, + statusPanel: this.createStatusPanel(), + sourcePanel: this.createSourcePanel(), + configPanel: this.createConfigPanel(), + helpPanel: this.createHelpPanel(), + }; + + sidebar.append( + this.elements.statusPanel, + this.elements.sourcePanel, + this.elements.configPanel, + this.elements.helpPanel, + ); + + return this.elements; + } + + createTransportControls() { + const wrapper = createElement('div', 'control-group'); + const pauseButton = createElement('button', 'control-button', 'Pause'); + const resetButton = createElement('button', 'control-button', 'Reset'); + const speedLabel = createElement('span', null, 'Speed'); + const speedInput = document.createElement('input'); + speedInput.type = 'range'; + speedInput.min = '0.1'; + speedInput.max = '6'; + speedInput.step = '0.1'; + const speedValue = createElement('span', null, '1.0×'); + + pauseButton.addEventListener('click', () => this.actions.togglePause()); + resetButton.addEventListener('click', () => this.actions.resetScene()); + speedInput.addEventListener('input', (event) => this.actions.setSpeed(Number(event.target.value))); + + wrapper.append(pauseButton, resetButton, speedLabel, speedInput, speedValue); + this.elements.pauseButton = pauseButton; + this.elements.resetButton = resetButton; + this.elements.speedInput = speedInput; + this.elements.speedValue = speedValue; + return wrapper; + } + + createPanelToggles() { + const wrapper = createElement('div', 'control-group'); + const panelIds = ['status', 'source', 'config', 'help']; + this.elements.panelButtons = {}; + + for (const panelId of panelIds) { + const button = createElement('button', 'panel-toggle', panelId); + button.addEventListener('click', () => this.actions.togglePanel(panelId)); + this.elements.panelButtons[panelId] = button; + wrapper.append(button); + } + + return wrapper; + } + + createStatusPanel() { + const panel = createElement('section', 'panel'); + panel.innerHTML = ` +

Status

+
+
Renderer
+
Real time
+
Real elapsed
+
Plot time
+
Playback
+
Points
+
+ `; + return panel; + } + + createSourcePanel() { + const panel = createElement('section', 'panel'); + panel.innerHTML = ` +

Data Source

+
+
Signal A
+
+ +
+
+ + + + +
+
+ + +
+
+
+ + +
+
+
+
+
Signal B
+
+ +
+
+ + + + +
+
+ + +
+
+
+ + +
+
+
+ `; + + panel.querySelectorAll('[data-source-field]').forEach((input) => { + const eventName = input.tagName === 'SELECT' ? 'change' : 'input'; + input.addEventListener(eventName, () => { + const sourceKey = input.getAttribute('data-source-key'); + const field = input.getAttribute('data-source-field'); + const value = readControlValue(input); + this.actions.updateSource(sourceKey, field, value); + }); + }); + + panel.querySelectorAll('[data-source-file]').forEach((input) => { + input.addEventListener('change', async () => { + const sourceKey = input.getAttribute('data-source-key'); + const file = input.files?.[0]; + if (!file) { + return; + } + + await this.actions.loadSourceFile(sourceKey, file); + input.value = ''; + }); + }); + + return panel; + } + + createConfigPanel() { + const panel = createElement('section', 'panel'); + panel.innerHTML = ` +

Config

+
+ + +
+ Show grid + +
+
+ Show points + +
+
+
+
Graph routing
+
+ + + + +
+
+ `; + + panel.querySelectorAll('[data-plot-field]').forEach((input) => { + const eventName = input instanceof HTMLInputElement && input.type === 'checkbox' ? 'change' : 'input'; + input.addEventListener(eventName, () => { + const field = input.getAttribute('data-plot-field'); + const value = readControlValue(input); + this.actions.updatePlot(field, value); + }); + }); + + panel.querySelectorAll('[data-graph-field]').forEach((input) => { + input.addEventListener('change', () => { + const graphId = input.getAttribute('data-graph-id'); + const field = input.getAttribute('data-graph-field'); + this.actions.updateGraph(graphId, field, input.value); + }); + }); + + return panel; + } + + createHelpPanel() { + const panel = createElement('section', 'panel'); + panel.innerHTML = ` +

Help

+
    +
  1. Each signal can be synthetic or file-backed CSV replay.
  2. +
  3. Each graph can target Signal A or Signal B independently.
  4. +
  5. Each graph can render raw, delta, or smoothed data.
  6. +
  7. Hover either trace to inspect the nearest synchronized sample.
  8. +
  9. Use pause and speed controls to inspect timing behavior.
  10. +
+ `; + return panel; + } + + sync(state, visiblePoints) { + this.elements.title.textContent = state.app.title; + this.elements.subtitle.textContent = 'Dual synchronized signal monitor'; + this.elements.pauseButton.textContent = state.time.paused ? 'Resume' : 'Pause'; + setToggleState(this.elements.pauseButton, state.time.paused); + syncControlValue(this.elements.speedInput, state.time.speed); + this.elements.speedValue.textContent = `${state.time.speed.toFixed(1)}×`; + + const statusFields = this.elements.statusPanel.querySelectorAll('[data-field]'); + const fieldMap = Object.fromEntries(Array.from(statusFields).map((field) => [field.getAttribute('data-field'), field])); + fieldMap.renderer.textContent = state.app.renderer; + fieldMap.realTime.textContent = formatWallClock(state.time.realNowMs); + fieldMap.realElapsed.textContent = formatDuration(state.time.realElapsedMs); + fieldMap.plotTime.textContent = formatDuration(state.time.plotTimeMs); + fieldMap.playback.textContent = state.time.paused ? 'Paused' : `${state.time.speed.toFixed(1)}×`; + fieldMap.points.textContent = typeof visiblePoints === 'object' + ? `${visiblePoints.primary} / ${visiblePoints.secondary}` + : `${visiblePoints}`; + + this.syncSourcePanel(state); + this.syncConfigPanel(state); + this.syncPanels(state); + this.syncTooltip(state); + } + + syncSourcePanel(state) { + Object.entries(state.sources).forEach(([sourceKey, sourceConfig]) => { + syncControlValue(this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="type"]`), sourceConfig.type); + syncControlValue(this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="preset"]`), sourceConfig.preset); + syncControlValue(this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="sampleRateHz"]`), sourceConfig.sampleRateHz); + syncControlValue(this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="amplitude"]`), sourceConfig.amplitude); + syncControlValue(this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="noise"]`), sourceConfig.noise); + const replayRateInput = this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="replayRate"]`); + if (replayRateInput) { + syncControlValue(replayRateInput, sourceConfig.replayRate ?? 1); + } + + const sourceSection = this.elements.sourcePanel.querySelector(`[data-source-config="${sourceKey}"]`); + sourceSection.querySelectorAll('[data-source-mode]').forEach((modeSection) => { + modeSection.hidden = modeSection.getAttribute('data-source-mode') !== sourceConfig.type; + }); + + const meta = this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-meta]`); + if (meta) { + if (sourceConfig.type === 'csv-replay') { + meta.innerHTML = sourceConfig.loadError + ? `${sourceConfig.loadError}` + : `${sourceConfig.dataFileName || 'No file loaded'}${sourceConfig.datasetPointCount ? ` · ${sourceConfig.datasetPointCount} pts · ${formatDuration(sourceConfig.datasetDurationMs || 0)}` : ''}`; + } else if (sourceConfig.type === 'websocket') { + meta.textContent = ''; + } else { + meta.textContent = 'Generates data procedurally in-browser'; + } + } + + const wsUrlInput = this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="wsUrl"]`); + const wsReconnectInput = this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="wsReconnectMs"]`); + const wsMeta = this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-ws-meta]`); + if (wsUrlInput) { + syncControlValue(wsUrlInput, sourceConfig.wsUrl ?? ''); + } + if (wsReconnectInput) { + syncControlValue(wsReconnectInput, sourceConfig.wsReconnectMs ?? 2000); + } + if (wsMeta) { + wsMeta.innerHTML = sourceConfig.type === 'websocket' + ? `status: ${sourceConfig.wsStatus || 'idle'}${sourceConfig.wsStatusDetail ? ` · ${sourceConfig.wsStatusDetail}` : ''}` + : ''; + } + }); + } + + syncConfigPanel(state) { + syncControlValue(this.elements.configPanel.querySelector('[data-plot-field="windowDurationMs"]'), state.plot.windowDurationMs); + syncControlValue(this.elements.configPanel.querySelector('[data-plot-field="maxPoints"]'), state.plot.maxPoints); + syncControlValue(this.elements.configPanel.querySelector('[data-plot-field="showGrid"]'), state.plot.showGrid); + syncControlValue(this.elements.configPanel.querySelector('[data-plot-field="showPoints"]'), state.plot.showPoints); + syncControlValue(this.elements.configPanel.querySelector('[data-graph-id="primary"][data-graph-field="sourceKey"]'), state.graphs.primary.sourceKey); + syncControlValue(this.elements.configPanel.querySelector('[data-graph-id="primary"][data-graph-field="transform"]'), state.graphs.primary.transform); + syncControlValue(this.elements.configPanel.querySelector('[data-graph-id="secondary"][data-graph-field="sourceKey"]'), state.graphs.secondary.sourceKey); + syncControlValue(this.elements.configPanel.querySelector('[data-graph-id="secondary"][data-graph-field="transform"]'), state.graphs.secondary.transform); + } + + syncPanels(state) { + const panelMap = { + status: this.elements.statusPanel, + source: this.elements.sourcePanel, + config: this.elements.configPanel, + help: this.elements.helpPanel, + }; + + for (const [panelId, panelState] of Object.entries(state.panels)) { + panelMap[panelId].hidden = !panelState.visible; + setToggleState(this.elements.panelButtons[panelId], panelState.visible); + } + } + + syncTooltip(state) { + const tooltipState = state.plot.tooltip; + this.elements.primaryTooltip.hidden = true; + this.elements.secondaryTooltip.hidden = true; + + if (!tooltipState.visible || !tooltipState.point) { + return; + } + + const tooltip = tooltipState.panelId === 'secondary' + ? this.elements.secondaryTooltip + : this.elements.primaryTooltip; + + tooltip.hidden = false; + tooltip.style.left = `${tooltipState.x}px`; + tooltip.style.top = `${tooltipState.y}px`; + tooltip.innerHTML = ` +
Hovered sample
+
Panel${tooltipState.panelLabel ?? 'Primary'}
+
Plot time${formatDuration(tooltipState.point.timeMs)}
+
Value${formatValue(tooltipState.point.value)}
+
Source${tooltipState.point.sourceId}
+ ${tooltipState.linkedPoint ? `
Linked panel${tooltipState.linkedPanelLabel ?? 'Linked'}
` : ''} + ${tooltipState.linkedPoint ? `
Linked value${formatValue(tooltipState.linkedPoint.value)}
` : ''} + `; + } +} diff --git a/src/utils-format.js b/src/utils-format.js new file mode 100644 index 0000000..f4eac88 --- /dev/null +++ b/src/utils-format.js @@ -0,0 +1,22 @@ +export function formatDuration(ms) { + const totalSeconds = Math.max(0, ms / 1000); + if (totalSeconds < 60) { + return `${totalSeconds.toFixed(2)}s`; + } + + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes}m ${seconds.toFixed(1)}s`; +} + +export function formatWallClock(timestampMs) { + return new Intl.DateTimeFormat(undefined, { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }).format(new Date(timestampMs)); +} + +export function formatValue(value) { + return Number.isFinite(value) ? value.toFixed(3) : '—'; +} diff --git a/src/waterfall.js b/src/waterfall.js new file mode 100644 index 0000000..bce0750 --- /dev/null +++ b/src/waterfall.js @@ -0,0 +1,219 @@ +import { Container, Graphics, Text } from 'pixi.js'; + +/** + * WaterfallGraph - A scrolling waterfall display + * Starts simple with basic line rendering + */ +export class WaterfallGraph { + constructor(config) { + this.x = config.x; + this.y = config.y; + this.width = config.width; + this.height = config.height; + this.title = config.title; + this.baseColor = config.color || 0xff6666; + + this.container = new Container(); + this.container.x = this.x; + this.container.y = this.y; + + // Graphics layers + this.borderGraphics = new Graphics(); + this.gridGraphics = new Graphics(); + this.linesGraphics = new Graphics(); + + this.container.addChild(this.gridGraphics); + this.container.addChild(this.linesGraphics); + this.container.addChild(this.borderGraphics); + + // Title text + this.titleText = new Text({ + text: this.title, + style: { + fontFamily: 'Arial', + fontSize: 18, + fill: 0xeeeeee, + } + }); + this.titleText.x = 10; + this.titleText.y = 10; + this.container.addChild(this.titleText); + + // Waterfall data + this.lines = []; + this.maxLines = 50; + this.pointsPerLine = 100; + this.frameCounter = 0; + + this.showGrid = true; + + // Time scaling and zoom + this.scrollSpeed = 1.0; // Speed multiplier for scrolling + this.baseScrollSpeed = 1.0; + this.verticalScale = 1.0; // Vertical zoom: >1 = zoomed in (see less history), <1 = zoomed out (see more) + + this.draw(); + } + + draw() { + this.drawBorder(); + this.drawGrid(); + } + + drawBorder() { + this.borderGraphics.clear(); + this.borderGraphics.rect(0, 0, this.width, this.height); + this.borderGraphics.stroke({ width: 2, color: 0x606070 }); + } + + drawGrid() { + this.gridGraphics.clear(); + + if (!this.showGrid) return; + + this.gridGraphics.alpha = 0.3; + + const divisions = 10; + const color = 0x4a7a9a; + + // Vertical lines + for (let i = 0; i <= divisions; i++) { + const x = (i / divisions) * this.width; + this.gridGraphics.moveTo(x, 0); + this.gridGraphics.lineTo(x, this.height); + this.gridGraphics.stroke({ width: 1, color }); + } + + // Horizontal lines + for (let i = 0; i <= divisions; i++) { + const y = (i / divisions) * this.height; + this.gridGraphics.moveTo(0, y); + this.gridGraphics.lineTo(this.width, y); + this.gridGraphics.stroke({ width: 1, color }); + } + } + + update(time, graphIdx) { + this.frameCounter++; + + // Add new line every 10 frames + if (this.frameCounter % 10 === 0 && this.lines.length < this.maxLines) { + this.addLine(time, graphIdx); + } + + // Scroll existing lines down + this.scrollLines(); + + // Remove off-screen lines + this.lines = this.lines.filter(line => line.yOffset < this.height + 50); + + // Redraw all lines + this.drawLines(); + } + + addLine(time, graphIdx) { + const line = { + points: [], + yOffset: 0, + color: this.generateColor(time), + }; + + // Generate sine wave points + const phase = time + (graphIdx * 2); + const freq = 2.0 + Math.sin(time * 0.5 + graphIdx) * 1.0; + + for (let i = 0; i < this.pointsPerLine; i++) { + const x = (i / this.pointsPerLine) * this.width; + const normalizedX = (i / this.pointsPerLine) * 2 - 1; // -1 to 1 + const y = Math.sin(i * 0.1 * freq + phase) * 30; // Amplitude in pixels + + line.points.push({ x, y }); + } + + this.lines.push(line); + } + + scrollLines() { + const speed = this.baseScrollSpeed * this.scrollSpeed; + this.lines.forEach(line => { + line.yOffset += speed; + }); + } + + setScrollSpeed(speed) { + // Clamp between 0.1 (slow) and 5.0 (fast) + this.scrollSpeed = Math.max(0.1, Math.min(5.0, speed)); + } + + getScrollSpeed() { + return this.scrollSpeed; + } + + setVerticalScale(scale) { + // Clamp between 0.2 (zoomed out, see more history) and 3.0 (zoomed in, see less) + this.verticalScale = Math.max(0.2, Math.min(3.0, scale)); + } + + getVerticalScale() { + return this.verticalScale; + } + + drawLines() { + this.linesGraphics.clear(); + + for (const line of this.lines) { + if (line.points.length < 2) continue; + + // Apply vertical scale to y positions + // Current time is at top (y=0), older data has larger yOffset + const scaledYOffset = line.yOffset * this.verticalScale; + + // Start path + const firstPoint = line.points[0]; + this.linesGraphics.moveTo(firstPoint.x, firstPoint.y + scaledYOffset); + + // Draw line strip + for (let i = 1; i < line.points.length; i++) { + const point = line.points[i]; + this.linesGraphics.lineTo(point.x, point.y + scaledYOffset); + } + + this.linesGraphics.stroke({ width: 2, color: line.color }); + } + } + + generateColor(time) { + // Cycle through colors based on time + const hue = (time * 0.1) % 1.0; + const r = Math.floor(Math.abs(Math.sin(hue * Math.PI * 2)) * 255); + const g = Math.floor(Math.abs(Math.sin((hue + 0.33) * Math.PI * 2)) * 255); + const b = Math.floor(Math.abs(Math.sin((hue + 0.66) * Math.PI * 2)) * 255); + + return (r << 16) | (g << 8) | b; + } + + setGridVisible(visible) { + this.showGrid = visible; + this.drawGrid(); + } + + resize(x, y, width, height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + + this.container.x = x; + this.container.y = y; + + this.draw(); + } + + getVertexCount() { + return this.lines.reduce((sum, line) => sum + line.points.length, 0); + } + + getLineCount() { + return this.lines.length; + } +} diff --git a/web-timeplot/.gitignore b/web-timeplot/.gitignore deleted file mode 100644 index 8b13789..0000000 --- a/web-timeplot/.gitignore +++ /dev/null @@ -1 +0,0 @@ - diff --git a/web-timeplot/AGENTS.md b/web-timeplot/AGENTS.md deleted file mode 100644 index 4926365..0000000 --- a/web-timeplot/AGENTS.md +++ /dev/null @@ -1,46 +0,0 @@ -## Features to implement -### some of the features that i want to implement next - -- time speed adjustment -- pause and resume time -- label of current real time -- label of current plot time -- hover tooltip for data points -- data input system -- multiple panels that can be toggled on and off - - use extension scaffolding src as example https://github.com/MoebiusSolutions/extension-scaffold/tree/main - - possibly use ES directly, but ideally make own system if it can be better and simpler -- config panel - -## Systems - dividing up concerns in a sensible, performant, and easy-to-maintain-and-work-with way -### my initial thinking on the concerns of different architectural components. this very well may change, and each thing does not necessarily map to its own file or even its own class. - -- local configuration: read a config file -- state management: - - user preferences - - service config (websocket url, remote api endpoints) - - UI config (what panels acive, ) - - database stuff - - plots (which attributes of which input data-type are which graph axes, current time range viewable, ) - - data input stuff (structure of input datapoints, source, metadata) - - health (service connections, framerate, db access) - - -- application event system (to notify of state updates) -- rendering -- input data processing -- HID input handling - - input controller maps -- plot handling -- DB handling -- disk handling -- service handling (websockets, mqtt, HTTP REST) - -## Some important data structures used in the program - -- input actions: a set of "single action that the user can perform" -- user input: a single event of user input via hardware like key press or click or move joystick -- input action map: a map of "set of "user input"" => "input action" -- ui element definition: - -### Notes -- we will need a good system for state management, as the state will grow fairly large, including things like user preferences, UI config (what panels are displayed), \ No newline at end of file diff --git a/web-timeplot/ARCHITECTURE.md b/web-timeplot/ARCHITECTURE.md deleted file mode 100644 index 73c4cb6..0000000 --- a/web-timeplot/ARCHITECTURE.md +++ /dev/null @@ -1,194 +0,0 @@ -# TimePlot Architecture - -## Overview - -The restarted TimePlot app is built around five small systems: - -1. **Store** — single source of truth for app state -2. **Time controller** — advances real time and plot time -3. **Source registry** — owns active data source lifecycle -4. **Plot view** — renders visible samples and handles hover picking -5. **Panel manager** — builds the DOM shell and user controls - -The current implementation is intentionally compact, but each system is already separated enough to grow without turning the app into a monolith again. - -Core workspace configuration is also persisted in `localStorage`, so plot settings, routing, and source setup survive reloads without persisting transient runtime state. - -## Runtime flow - -```text -TimeController.tick() - ↓ -Store.time updated - ↓ -SourceRegistry.update(plotTime) - ↓ -Synthetic / CSV replay / WebSocket sources emit samples - ↓ -PlotBuffer stores bounded history - ↓ -TimeplotView renders visible window - ↓ -PanelManager reflects status + tooltip -``` - -## Core principles - -### 1. Time is explicit - -Plot time is not inferred from frame count or rendering. It is advanced by `TimeController`, which makes features like pause, speed changes, replay, and stepping straightforward. - -### 2. Data sources are replaceable - -`SourceRegistry` talks to the active source through a tiny shared contract: - -- `start(startTimeMs)` -- `update(currentPlotTimeMs)` -- `reset(startTimeMs)` -- `updateConfig(partialConfig)` - -That keeps future WebSocket, file replay, database, or simulated sources easy to add. - -### 3. Rendering stays focused - -`TimeplotView` does not own application state or source orchestration. It receives state plus visible points and turns that into pixels. Hover detection also lives close to rendering because it depends on screen-space positions. - -### 4. UI panels stay in the DOM - -The plot is GPU-rendered with PixiJS. Controls, labels, and config panels stay in regular DOM so they are easy to iterate on, inspect, and restyle. - -### 5. Composition happens at the edge - -`create-app.js` is the composition root. It wires together store, time, sources, plot, UI, keyboard shortcuts, and the frame loop. That keeps the rest of the modules simple and testable. - -## Current state shape - -```js -{ - app: { title, renderer }, - time: { - realNowMs, - realElapsedMs, - plotTimeMs, - speed, - paused, - }, - plot: { - showGrid, - showPoints, - windowDurationMs, - maxPoints, - valueRange, - hoveredPoint, - tooltip, - }, - sources: { - signalA: { - type, - preset, - sampleRateHz, - amplitude, - noise, - replayRate, - wsUrl, - wsReconnectMs, - }, - signalB: { - ... - }, - }, - graphs: { - primary: { sourceKey, transform, title }, - secondary: { sourceKey, transform, title }, - }, - panels: { - status, - source, - config, - help, - }, -} -``` - -## Modules - -### `src/core/store.js` - -A tiny centralized store. It currently favors clarity over abstraction-heavy patterns. - -### `src/core/time-controller.js` - -Owns playback semantics: - -- pause/resume -- speed control -- plot time reset -- frame-to-frame delta handling - -### `src/data/synthetic-wave-source.js` - -Generates sample streams from a preset waveform. Right now it supports: - -- `telemetry` -- `chirp` -- `burst` - -### `src/data/csv-replay-source.js` - -Replays uploaded CSV datasets on the shared plot timebase. - -### `src/data/websocket-source.js` - -Streams live samples from a WebSocket server and reconnects automatically. - -### `src/plot/plot-buffer.js` - -Maintains bounded history so rendering and hover picking only operate on a manageable number of samples. - -### `src/plot/timeplot-view.js` - -Owns Pixi initialization, plotting, grid drawing, and nearest-point hover selection. - -### `src/ui/panel-manager.js` - -Creates: - -- top transport bar -- panel toggle buttons -- status panel -- data source panel -- config panel -- help panel -- floating tooltip - -## Why this is a better baseline - -The old project had useful ideas but too many concerns were mixed together. The new baseline is better because: - -- transport logic is separate from rendering -- data generation is separate from app wiring -- UI is separate from GPU drawing -- state is centralized and observable -- persisted configuration is separated from transient runtime state -- adding a new source or panel no longer requires rewriting the whole app - -## Recommended next steps - -### Near term - -- add persisted settings for panel visibility and playback preferences -- support multiple plot panes from a shared timebase -- add line/series definitions instead of a single hard-coded signal - -### Medium term - -- add schema-aware input adapters -- add WebSocket and replay-file sources -- add panel docking/layout persistence -- add markers, cursors, and annotations - -### Longer term - -- multi-stream synchronization -- richer interaction model for HID input mapping -- plug-in style source and panel registration diff --git a/web-timeplot/PROTOTYPING.md b/web-timeplot/PROTOTYPING.md deleted file mode 100644 index e220f9a..0000000 --- a/web-timeplot/PROTOTYPING.md +++ /dev/null @@ -1,146 +0,0 @@ -# PixiJS Prototyping Framework - -A minimal PixiJS framework with core architecture patterns (DOM initialization, Service initialization, State management) for rapid prototyping. - -## Quick Start - -```bash -npm run dev -``` - -Open browser to `http://localhost:5173/` - -## Architecture - -The framework follows a clean initialization pattern: - -1. **DOM Initialization** - Reference DOM elements -2. **Renderer Initialization** - Set up PixiJS with WebGPU/WebGL -3. **Services Initialization** - Start the update loop - -## Global Objects (Available in Console) - -- `window.PIXI` - Complete PixiJS namespace -- `window.pixiApp` - PixiJS Application instance -- `window.state` - StateManager instance (reactive state) -- `window.log` - Logger function - -## Rapid Prototyping Examples - -### Example 1: Draw a Rectangle - -Open browser console: - -```javascript -const graphics = new PIXI.Graphics(); -graphics.rect(100, 100, 200, 150); -graphics.fill(0xff0000); -pixiApp.stage.addChild(graphics); -``` - -### Example 2: Animated Sprite - -```javascript -const graphics = new PIXI.Graphics(); -graphics.circle(0, 0, 50); -graphics.fill(0x00ff00); -pixiApp.stage.addChild(graphics); - -// Add to update loop in main.js: -// graphics.x = Math.sin(state.state.time.current) * 200 + pixiApp.screen.width / 2; -// graphics.y = pixiApp.screen.height / 2; -``` - -### Example 3: Using State System - -The framework includes a reactive state manager: - -```javascript -// Listen to state changes -state.on('time.current', ({ value }) => { - console.log('Time:', value); -}); - -// Modify state (triggers listeners) -state.state.time.speed = 2.0; // Double speed - -// Toggle pause -state.togglePause(); -``` - -### Example 4: Register Input Actions - -```javascript -// In main.js, add to setupControls(): -state.registerAction('myAction', () => { - log('Action triggered!'); -}); - -state.mapKey('KeyP', 'myAction'); -``` - -## Modifying the Update Loop - -Edit `/src/main.js` function `update()`: - -```javascript -function update() { - state.incrementTime(0.016); // ~60fps increment - state.updateRealElapsed(); - state.state.rendering.frameCounter++; - - // YOUR PROTOTYPE CODE GOES HERE - // Example: - mySprite.rotation += 0.01; - myGraphics.x = Math.sin(state.state.time.current) * 100; -} -``` - -## State Structure - -```javascript -state.state = { - userPrefs: { - showGrid: true, - showMetrics: true, - theme: 'dark', - // ... persisted to localStorage - }, - - uiConfig: { - canvasWidth: number, - canvasHeight: number, - // ... - }, - - time: { - current: number, // Increments every frame - realElapsed: number, // Real seconds since start - speed: number, // Time multiplier - isPaused: boolean, - }, - - rendering: { - rendererType: 'webgpu' | 'webgl', - frameCounter: number, - }, - - health: { - fps: number, - updateMs: number, - renderMs: number, - }, -} -``` - -## Tips - -1. **Use the console** - All major objects are exposed globally -2. **Hot reload** - Vite will automatically reload on file changes -3. **State persistence** - userPrefs automatically save to localStorage -4. **Responsive** - Canvas automatically resizes with window -5. **WebGPU fallback** - Automatically falls back to WebGL if WebGPU unavailable - -## Clean Slate - -The framework intentionally draws nothing by default. Start adding your PixiJS objects and see results immediately. diff --git a/web-timeplot/README.md b/web-timeplot/README.md deleted file mode 100644 index 25dfb80..0000000 --- a/web-timeplot/README.md +++ /dev/null @@ -1,147 +0,0 @@ -# TimePlot - -TimePlot is now a clean restart: a small PixiJS time-series sandbox built around a simple state core, a pluggable data source layer, and toggleable UI panels. - -## What it does - -- Real-time scrolling plot with PixiJS -- Pause/resume plot time -- Adjustable playback speed -- Current real-time and plot-time labels -- Hover tooltip for data points -- Modular synthetic data input system -- CSV replay sources -- WebSocket live sources -- Persisted workspace settings -- Toggleable side panels for status, source config, app config, and help - -## Getting started - -```bash -bun install -bun run dev -``` - -Production build: - -```bash -bun run build -bun run preview -``` - -Demo WebSocket source: - -```bash -bun run ws:demo -``` - -## Controls - -- `Space` — pause/resume -- `[` — slow down playback -- `]` — speed up playback -- `G` — toggle grid -- Hover plot — inspect nearest sample - -## Demo data - -Sample CSV replay files are included in [public/demo-data](public/demo-data): - -- [public/demo-data/telemetry-sweep.csv](public/demo-data/telemetry-sweep.csv) -- [public/demo-data/chirp-ramp.csv](public/demo-data/chirp-ramp.csv) -- [public/demo-data/step-bursts.csv](public/demo-data/step-bursts.csv) - -Use the `CSV replay` source type in the sidebar and upload one of those files. - -## WebSocket source - -TimePlot includes a local demo WebSocket server in [scripts/demo-websocket-server.mjs](scripts/demo-websocket-server.mjs). - -Start it with: - -```bash -bun run ws:demo -``` - -Then set a signal source to `WebSocket` and use `ws://localhost:8080`. - -Optional environment variables: - -```bash -PORT=8090 TIMEPLOT_PROFILE=chirp TIMEPLOT_INTERVAL_MS=50 bun run ws:demo -``` - -Supported demo profiles: - -- `telemetry` -- `chirp` -- `steps` -- `burst` - -Protocol details and accepted message formats are documented in [WEBSOCKET_FORMAT.md](WEBSOCKET_FORMAT.md). - -## Persistence - -TimePlot persists core workspace settings in `localStorage`, including: - -- plot display settings -- playback speed -- panel visibility -- graph routing and transforms -- source configuration such as presets and WebSocket URLs - -CSV replay files themselves are not persisted in storage. After a reload, TimePlot remembers which CSV file was selected but asks you to reload the file data. - -## Project structure - -```text -src/ -├── app/ -│ └── create-app.js # application composition root -├── core/ -│ ├── event-bus.js # lightweight pub/sub -│ ├── store.js # centralized app state -│ └── time-controller.js # real time + plot time transport -├── data/ -│ ├── base-source.js # source interface -│ ├── csv-replay-source.js -│ ├── parse-replay-csv.js -│ ├── source-registry.js # source lifecycle + routing -│ ├── synthetic-wave-source.js -│ └── websocket-source.js -├── plot/ -│ ├── plot-buffer.js # bounded in-memory sample history -│ └── timeplot-view.js # Pixi rendering + hover picking -├── ui/ -│ └── panel-manager.js # DOM shell, controls, panels, tooltip -├── bootstrap.js # startup entry -├── main.js # compatibility shim to bootstrap -├── styles.css # global UI styling -└── utils-format.js # display formatting helpers - -public/ -└── demo-data/ # sample CSV replay fixtures - -scripts/ -└── demo-websocket-server.mjs -``` - -## Design direction - -This restart intentionally optimizes for a strong foundation instead of feature sprawl: - -- transport and time are first-class systems -- data generation is isolated from rendering -- the plot owns visualization only -- DOM panels handle controls and diagnostics -- app composition happens in one predictable bootstrap path -- synthetic, file replay, and WebSocket sources share one source abstraction -- core workspace configuration survives reloads - -## Next good additions - -- richer external data sources (REST replay, binary streams, custom adapters) -- richer panel layout system with docking/persistence -- plot annotations and multiple stacked plots -- configurable schemas for incoming data types -- persistent user settings diff --git a/web-timeplot/WEBSOCKET_FORMAT.md b/web-timeplot/WEBSOCKET_FORMAT.md deleted file mode 100644 index 93eead2..0000000 --- a/web-timeplot/WEBSOCKET_FORMAT.md +++ /dev/null @@ -1,117 +0,0 @@ -# WebSocket Data Format - -TimePlot's WebSocket source accepts UTF-8 text frames whose contents can be parsed into one of the supported payload shapes below. - -## Recommended payload - -Send one JSON object per message: - -```json -{ - "timestampMs": 1250, - "value": 0.482 -} -``` - -Fields: - -- `value` — required numeric sample value -- `timestampMs` — optional numeric source timestamp in milliseconds - -If `timestampMs` is present, TimePlot uses it to preserve the source timing relationship and aligns it onto the app's plot timebase. -If `timestampMs` is omitted, TimePlot stamps the sample at the current plot time when the message arrives. - -## Other accepted object keys - -TimePlot also accepts these alternate numeric field names: - -- value fields: `value`, `y`, `signal`, `data` -- time fields: `timeMs`, `timestampMs`, `timestamp`, `t` - -Examples: - -```json -{"y": 0.91, "t": 2040} -``` - -```json -{"signal": -0.13, "timestamp": 9810} -``` - -## Arrays - -A single message may contain an array of supported payloads: - -```json -[ - {"timestampMs": 1000, "value": 0.2}, - {"timestampMs": 1100, "value": 0.3}, - {"timestampMs": 1200, "value": 0.5} -] -``` - -This is useful for batching. - -## Bare numeric messages - -These also work, though JSON objects are preferred: - -```text -0.418 -``` - -or: - -```json -42.5 -``` - -These are treated as samples without an explicit timestamp. - -## Unsupported / ignored messages - -Messages are ignored if TimePlot cannot find a numeric sample value. -Examples of ignored payloads: - -- empty strings -- non-numeric strings -- JSON objects without a numeric `value`-like field - -## Demo server compatibility - -The included demo server sends messages like: - -```json -{ - "timestampMs": 1870, - "value": 0.735812, - "sequence": 19, - "profile": "telemetry" -} -``` - -Extra fields are safe. TimePlot ignores anything it does not need. - -## Running the demo server - -```bash -bun run ws:demo -``` - -Environment options: - -- `PORT` — default `8080` -- `TIMEPLOT_PROFILE` — `telemetry`, `chirp`, `steps`, or `burst` -- `TIMEPLOT_INTERVAL_MS` — message interval in milliseconds - -Example: - -```bash -PORT=8090 TIMEPLOT_PROFILE=chirp TIMEPLOT_INTERVAL_MS=50 bun run ws:demo -``` - -Then set a signal source type to `WebSocket` and point it at: - -```text -ws://localhost:8090 -``` diff --git a/web-timeplot/bun.lock b/web-timeplot/bun.lock deleted file mode 100644 index 82f672d..0000000 --- a/web-timeplot/bun.lock +++ /dev/null @@ -1,150 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 0, - "workspaces": { - "": { - "name": "web-timeplot", - "dependencies": { - "pixi.js": "^8.0.0", - }, - "devDependencies": { - "vite": "^5.0.0", - }, - }, - }, - "packages": { - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], - - "@pixi/colord": ["@pixi/colord@2.9.6", "", {}, "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.3", "", { "os": "android", "cpu": "arm" }, "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.3", "", { "os": "android", "cpu": "arm64" }, "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.3", "", { "os": "linux", "cpu": "arm" }, "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.3", "", { "os": "linux", "cpu": "arm" }, "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.3", "", { "os": "linux", "cpu": "none" }, "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.3", "", { "os": "linux", "cpu": "none" }, "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.3", "", { "os": "linux", "cpu": "none" }, "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.3", "", { "os": "linux", "cpu": "x64" }, "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.3", "", { "os": "none", "cpu": "arm64" }, "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.3", "", { "os": "win32", "cpu": "x64" }, "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.3", "", { "os": "win32", "cpu": "x64" }, "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA=="], - - "@types/css-font-loading-module": ["@types/css-font-loading-module@0.0.12", "", {}, "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA=="], - - "@types/earcut": ["@types/earcut@3.0.0", "", {}, "sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "@webgpu/types": ["@webgpu/types@0.1.65", "", {}, "sha512-cYrHab4d6wuVvDW5tdsfI6/o6vcLMDe6w2Citd1oS51Xxu2ycLCnVo4fqwujfKWijrZMInTJIKcXxteoy21nVA=="], - - "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], - - "earcut": ["earcut@3.0.2", "", {}, "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ=="], - - "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": "bin/esbuild" }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], - - "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "gifuct-js": ["gifuct-js@2.1.2", "", { "dependencies": { "js-binary-schema-parser": "^2.0.3" } }, "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg=="], - - "ismobilejs": ["ismobilejs@1.1.1", "", {}, "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw=="], - - "js-binary-schema-parser": ["js-binary-schema-parser@2.0.3", "", {}, "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "parse-svg-path": ["parse-svg-path@0.1.2", "", {}, "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "pixi.js": ["pixi.js@8.13.2", "", { "dependencies": { "@pixi/colord": "^2.9.6", "@types/css-font-loading-module": "^0.0.12", "@types/earcut": "^3.0.0", "@webgpu/types": "^0.1.40", "@xmldom/xmldom": "^0.8.10", "earcut": "^3.0.2", "eventemitter3": "^5.0.1", "gifuct-js": "^2.1.2", "ismobilejs": "^1.1.1", "parse-svg-path": "^0.1.2", "tiny-lru": "^11.4.5" } }, "sha512-9KVGZ4a99TA5SwUEWs9m5gliX6XUCS1aGc/DOPsXxpqLMDRa+FhzpT5ao9z1UwLYJkSvt3rcQs+aZXECBHSSHg=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - - "rollup": ["rollup@4.52.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.3", "@rollup/rollup-android-arm64": "4.52.3", "@rollup/rollup-darwin-arm64": "4.52.3", "@rollup/rollup-darwin-x64": "4.52.3", "@rollup/rollup-freebsd-arm64": "4.52.3", "@rollup/rollup-freebsd-x64": "4.52.3", "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", "@rollup/rollup-linux-arm-musleabihf": "4.52.3", "@rollup/rollup-linux-arm64-gnu": "4.52.3", "@rollup/rollup-linux-arm64-musl": "4.52.3", "@rollup/rollup-linux-loong64-gnu": "4.52.3", "@rollup/rollup-linux-ppc64-gnu": "4.52.3", "@rollup/rollup-linux-riscv64-gnu": "4.52.3", "@rollup/rollup-linux-riscv64-musl": "4.52.3", "@rollup/rollup-linux-s390x-gnu": "4.52.3", "@rollup/rollup-linux-x64-gnu": "4.52.3", "@rollup/rollup-linux-x64-musl": "4.52.3", "@rollup/rollup-openharmony-arm64": "4.52.3", "@rollup/rollup-win32-arm64-msvc": "4.52.3", "@rollup/rollup-win32-ia32-msvc": "4.52.3", "@rollup/rollup-win32-x64-gnu": "4.52.3", "@rollup/rollup-win32-x64-msvc": "4.52.3", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "tiny-lru": ["tiny-lru@11.4.5", "", {}, "sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw=="], - - "vite": ["vite@5.4.20", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g=="], - } -} diff --git a/web-timeplot/index.html b/web-timeplot/index.html deleted file mode 100644 index 76e8b87..0000000 --- a/web-timeplot/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - TimePlot - - -
- - - diff --git a/web-timeplot/package-lock.json b/web-timeplot/package-lock.json deleted file mode 100644 index 7ce7bea..0000000 --- a/web-timeplot/package-lock.json +++ /dev/null @@ -1,1071 +0,0 @@ -{ - "name": "web-timeplot", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "web-timeplot", - "version": "0.1.0", - "dependencies": { - "pixi.js": "^8.0.0", - "ws": "^8.20.0" - }, - "devDependencies": { - "vite": "^5.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@pixi/colord": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", - "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==", - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", - "integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz", - "integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz", - "integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz", - "integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz", - "integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz", - "integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz", - "integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz", - "integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz", - "integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz", - "integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz", - "integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz", - "integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz", - "integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz", - "integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz", - "integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz", - "integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz", - "integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz", - "integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz", - "integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz", - "integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz", - "integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz", - "integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/css-font-loading-module": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", - "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==", - "license": "MIT" - }, - "node_modules/@types/earcut": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-3.0.0.tgz", - "integrity": "sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==", - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webgpu/types": { - "version": "0.1.65", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.65.tgz", - "integrity": "sha512-cYrHab4d6wuVvDW5tdsfI6/o6vcLMDe6w2Citd1oS51Xxu2ycLCnVo4fqwujfKWijrZMInTJIKcXxteoy21nVA==", - "license": "BSD-3-Clause" - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/earcut": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", - "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", - "license": "ISC" - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gifuct-js": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz", - "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==", - "license": "MIT", - "dependencies": { - "js-binary-schema-parser": "^2.0.3" - } - }, - "node_modules/ismobilejs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", - "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==", - "license": "MIT" - }, - "node_modules/js-binary-schema-parser": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz", - "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/parse-svg-path": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", - "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/pixi.js": { - "version": "8.13.2", - "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.13.2.tgz", - "integrity": "sha512-9KVGZ4a99TA5SwUEWs9m5gliX6XUCS1aGc/DOPsXxpqLMDRa+FhzpT5ao9z1UwLYJkSvt3rcQs+aZXECBHSSHg==", - "license": "MIT", - "dependencies": { - "@pixi/colord": "^2.9.6", - "@types/css-font-loading-module": "^0.0.12", - "@types/earcut": "^3.0.0", - "@webgpu/types": "^0.1.40", - "@xmldom/xmldom": "^0.8.10", - "earcut": "^3.0.2", - "eventemitter3": "^5.0.1", - "gifuct-js": "^2.1.2", - "ismobilejs": "^1.1.1", - "parse-svg-path": "^0.1.2", - "tiny-lru": "^11.4.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/pixijs" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/rollup": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", - "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.3", - "@rollup/rollup-android-arm64": "4.52.3", - "@rollup/rollup-darwin-arm64": "4.52.3", - "@rollup/rollup-darwin-x64": "4.52.3", - "@rollup/rollup-freebsd-arm64": "4.52.3", - "@rollup/rollup-freebsd-x64": "4.52.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", - "@rollup/rollup-linux-arm-musleabihf": "4.52.3", - "@rollup/rollup-linux-arm64-gnu": "4.52.3", - "@rollup/rollup-linux-arm64-musl": "4.52.3", - "@rollup/rollup-linux-loong64-gnu": "4.52.3", - "@rollup/rollup-linux-ppc64-gnu": "4.52.3", - "@rollup/rollup-linux-riscv64-gnu": "4.52.3", - "@rollup/rollup-linux-riscv64-musl": "4.52.3", - "@rollup/rollup-linux-s390x-gnu": "4.52.3", - "@rollup/rollup-linux-x64-gnu": "4.52.3", - "@rollup/rollup-linux-x64-musl": "4.52.3", - "@rollup/rollup-openharmony-arm64": "4.52.3", - "@rollup/rollup-win32-arm64-msvc": "4.52.3", - "@rollup/rollup-win32-ia32-msvc": "4.52.3", - "@rollup/rollup-win32-x64-gnu": "4.52.3", - "@rollup/rollup-win32-x64-msvc": "4.52.3", - "fsevents": "~2.3.2" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tiny-lru": { - "version": "11.4.5", - "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.5.tgz", - "integrity": "sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/vite": { - "version": "5.4.20", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", - "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - } - } -} diff --git a/web-timeplot/package.json b/web-timeplot/package.json deleted file mode 100644 index 9f4220f..0000000 --- a/web-timeplot/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "web-timeplot", - "version": "0.1.0", - "description": "PixiJS waterfall display with WebGPU/WebGL", - "type": "module", - "scripts": { - "dev": "vite --host", - "build": "vite build", - "preview": "vite preview", - "ws:demo": "node ./scripts/demo-websocket-server.mjs" - }, - "devDependencies": { - "vite": "^5.0.0" - }, - "dependencies": { - "pixi.js": "^8.0.0", - "ws": "^8.20.0" - } -} diff --git a/web-timeplot/public/demo-data/chirp-ramp.csv b/web-timeplot/public/demo-data/chirp-ramp.csv deleted file mode 100644 index 5e81c10..0000000 --- a/web-timeplot/public/demo-data/chirp-ramp.csv +++ /dev/null @@ -1,47 +0,0 @@ -time_ms,value -0,-0.04 -120,0.05 -240,0.11 -360,0.07 -480,-0.03 -600,-0.17 -720,-0.26 -840,-0.22 -960,-0.04 -1080,0.23 -1200,0.48 -1320,0.57 -1440,0.38 -1560,-0.01 -1680,-0.43 -1800,-0.67 -1920,-0.55 -2040,-0.07 -2160,0.53 -2280,0.89 -2400,0.76 -2520,0.16 -2640,-0.61 -2760,-1.01 -2880,-0.78 -3000,0.02 -3120,0.87 -3240,1.18 -3360,0.75 -3480,-0.21 -3600,-1.04 -3720,-1.21 -3840,-0.44 -3960,0.63 -4080,1.28 -4200,1.05 -4320,0.01 -4440,-1.01 -4560,-1.34 -4680,-0.69 -4800,0.47 -4920,1.31 -5040,1.26 -5160,0.31 -5280,-0.92 -5400,-1.43 diff --git a/web-timeplot/public/demo-data/step-bursts.csv b/web-timeplot/public/demo-data/step-bursts.csv deleted file mode 100644 index e9dbc3e..0000000 --- a/web-timeplot/public/demo-data/step-bursts.csv +++ /dev/null @@ -1,42 +0,0 @@ -time_ms,value -0,0.0 -200,0.0 -400,0.0 -600,0.4 -800,0.8 -1000,1.2 -1200,1.2 -1400,1.2 -1600,0.3 -1800,-0.2 -2000,-0.7 -2200,-1.1 -2400,-1.1 -2600,-0.5 -2800,0.1 -3000,0.6 -3200,1.0 -3400,0.5 -3600,-0.4 -3800,-1.0 -4000,-0.6 -4200,0.2 -4400,0.7 -4600,1.1 -4800,0.9 -5000,0.1 -5200,-0.8 -5400,-1.3 -5600,-0.9 -5800,-0.1 -6000,0.8 -6200,1.4 -6400,1.1 -6600,0.0 -6800,-0.9 -7000,-1.4 -7200,-1.0 -7400,-0.2 -7600,0.5 -7800,0.9 -8000,0.0 diff --git a/web-timeplot/public/demo-data/telemetry-sweep.csv b/web-timeplot/public/demo-data/telemetry-sweep.csv deleted file mode 100644 index 8c7d6e3..0000000 --- a/web-timeplot/public/demo-data/telemetry-sweep.csv +++ /dev/null @@ -1,42 +0,0 @@ -time_ms,value -0,0.12 -150,0.18 -300,0.31 -450,0.44 -600,0.52 -750,0.68 -900,0.83 -1050,0.96 -1200,1.04 -1350,1.08 -1500,1.01 -1650,0.92 -1800,0.77 -1950,0.58 -2100,0.34 -2250,0.12 -2400,-0.08 -2550,-0.22 -2700,-0.35 -2850,-0.48 -3000,-0.59 -3150,-0.66 -3300,-0.72 -3450,-0.64 -3600,-0.49 -3750,-0.27 -3900,-0.02 -4050,0.24 -4200,0.46 -4350,0.67 -4500,0.81 -4650,0.9 -4800,0.95 -4950,0.88 -5100,0.75 -5250,0.54 -5400,0.29 -5550,0.03 -5700,-0.2 -5850,-0.37 -6000,-0.48 diff --git a/web-timeplot/scripts/demo-websocket-server.mjs b/web-timeplot/scripts/demo-websocket-server.mjs deleted file mode 100644 index 1bee865..0000000 --- a/web-timeplot/scripts/demo-websocket-server.mjs +++ /dev/null @@ -1,131 +0,0 @@ -import { WebSocketServer } from 'ws'; - -const port = Number(process.env.PORT || 8080); -const profile = process.env.TIMEPLOT_PROFILE || 'telemetry'; -const sendIntervalMs = Number(process.env.TIMEPLOT_INTERVAL_MS || 100); -const logEvery = Number(process.env.TIMEPLOT_LOG_EVERY || 10); - -const wss = new WebSocketServer({ port }); -const startedAt = Date.now(); -let sampleIndex = 0; -let activeClientCount = 0; - -function log(message, details = '') { - const timestamp = new Date().toISOString(); - if (details) { - console.log(`[timeplot-ws ${timestamp}] ${message} ${details}`); - return; - } - - console.log(`[timeplot-ws ${timestamp}] ${message}`); -} - -function sampleTelemetry(seconds) { - return Math.sin(seconds * 2.2) + 0.35 * Math.cos(seconds * 6.4 + Math.sin(seconds * 0.8)) + 0.15 * Math.sin(seconds * 0.33); -} - -function sampleChirp(seconds) { - return 0.7 * Math.sin(seconds * seconds * 1.4) + 0.3 * Math.sin(seconds * 7.5); -} - -function sampleSteps(seconds) { - const phase = Math.floor((seconds % 8) / 1.0); - return [0, 0.4, 0.9, 1.2, 0.2, -0.6, -1.0, 0.3][phase] ?? 0; -} - -function sampleBurst(seconds) { - const burstPhase = (seconds % 6) - 1.5; - const burst = Math.sin(seconds * 9.5) * Math.exp(-(burstPhase ** 2) * 0.8); - return 0.45 * Math.sin(seconds * 2.1) + burst; -} - -function sampleValue(seconds) { - switch (profile) { - case 'chirp': - return sampleChirp(seconds); - case 'steps': - return sampleSteps(seconds); - case 'burst': - return sampleBurst(seconds); - case 'telemetry': - default: - return sampleTelemetry(seconds); - } -} - -function buildMessage() { - const timestampMs = Date.now() - startedAt; - const seconds = timestampMs / 1000; - sampleIndex += 1; - - return { - timestampMs, - value: Number(sampleValue(seconds).toFixed(6)), - sequence: sampleIndex, - profile, - }; -} - -const interval = setInterval(() => { - const message = buildMessage(); - const payload = JSON.stringify(message); - let sentCount = 0; - - for (const client of wss.clients) { - if (client.readyState === client.OPEN) { - client.send(payload); - sentCount += 1; - } - } - - if (message.sequence === 1 || (logEvery > 0 && message.sequence % logEvery === 0)) { - log( - 'broadcast', - `seq=${message.sequence} clients=${sentCount} timestampMs=${message.timestampMs} value=${message.value}`, - ); - } -}, sendIntervalMs); - -wss.on('connection', (socket, request) => { - const clientAddress = request.socket.remoteAddress || 'unknown'; - activeClientCount += 1; - log('client connected', `from=${clientAddress} activeClients=${activeClientCount}`); - - socket.send(JSON.stringify({ - timestampMs: 0, - value: 0, - sequence: 0, - profile, - message: 'connected', - })); - - socket.on('error', (error) => { - log('client error', `from=${clientAddress} error=${error.message}`); - }); - - socket.on('close', () => { - activeClientCount = Math.max(0, activeClientCount - 1); - log('client disconnected', `from=${clientAddress} activeClients=${activeClientCount}`); - }); -}); - -wss.on('error', (error) => { - log('server error', error.message); -}); - -wss.on('listening', () => { - log('listening', `url=ws://localhost:${port}`); - log('config', `profile=${profile} intervalMs=${sendIntervalMs} logEvery=${logEvery}`); -}); - -function shutdown() { - log('shutdown requested', `activeClients=${activeClientCount}`); - clearInterval(interval); - wss.close(() => { - log('server stopped'); - process.exit(0); - }); -} - -process.on('SIGINT', shutdown); -process.on('SIGTERM', shutdown); diff --git a/web-timeplot/src/app/create-app.js b/web-timeplot/src/app/create-app.js deleted file mode 100644 index 4f4f0fc..0000000 --- a/web-timeplot/src/app/create-app.js +++ /dev/null @@ -1,449 +0,0 @@ -import { EventBus } from '../core/event-bus.js'; -import { Store, createInitialState } from '../core/store.js'; -import { TimeController } from '../core/time-controller.js'; -import { PlotBuffer } from '../plot/plot-buffer.js'; -import { TimeplotView } from '../plot/timeplot-view.js'; -import { SourceRegistry } from '../data/source-registry.js'; -import { parseReplayCsv } from '../data/parse-replay-csv.js'; -import { PanelManager } from '../ui/panel-manager.js'; - -function clamp(value, min, max) { - return Math.min(max, Math.max(min, value)); -} - -function buildDeltaPoints(points) { - if (points.length < 2) { - return []; - } - - const derived = []; - for (let index = 1; index < points.length; index += 1) { - const previous = points[index - 1]; - const current = points[index]; - const deltaTime = Math.max(1, current.timeMs - previous.timeMs); - derived.push({ - ...current, - value: (current.value - previous.value) / deltaTime * 1000, - sourceId: `${current.sourceId}:delta`, - }); - } - - return derived; -} - -function buildSmoothedPoints(points, windowSize = 5) { - if (points.length === 0) { - return []; - } - - const smoothed = []; - for (let index = 0; index < points.length; index += 1) { - const start = Math.max(0, index - windowSize + 1); - const windowPoints = points.slice(start, index + 1); - const average = windowPoints.reduce((sum, point) => sum + point.value, 0) / windowPoints.length; - smoothed.push({ - ...points[index], - value: average, - sourceId: `${points[index].sourceId}:smooth`, - }); - } - - return smoothed; -} - -function transformPoints(points, transform) { - switch (transform) { - case 'delta': - return buildDeltaPoints(points); - case 'smooth': - return buildSmoothedPoints(points); - case 'raw': - default: - return points; - } -} - -function describeTransform(transform) { - switch (transform) { - case 'delta': - return 'Δvalue / second'; - case 'smooth': - return 'moving average'; - case 'raw': - default: - return 'raw signal'; - } -} - -function deriveValueRange(points, fallbackRange) { - if (points.length === 0) { - return fallbackRange; - } - - let min = Infinity; - let max = -Infinity; - for (const point of points) { - min = Math.min(min, point.value); - max = Math.max(max, point.value); - } - - const maxAbs = Math.max(Math.abs(min), Math.abs(max), 0.1); - return { - min: -maxAbs, - max: maxAbs, - }; -} - -function pickActiveHover(primaryCandidate, secondaryCandidate) { - if (!primaryCandidate && !secondaryCandidate) { - return null; - } - - if (primaryCandidate && !secondaryCandidate) { - return primaryCandidate; - } - - if (!primaryCandidate && secondaryCandidate) { - return secondaryCandidate; - } - - return primaryCandidate.lastPointerEventAt >= secondaryCandidate.lastPointerEventAt - ? primaryCandidate - : secondaryCandidate; -} - -export async function createApp(root) { - const bus = new EventBus(); - const store = new Store(createInitialState()); - const timeController = new TimeController(store); - const sourceBuffers = new Map(Object.keys(store.getState().sources).map((sourceKey) => [sourceKey, new PlotBuffer(store.getState().plot.maxPoints)])); - let sourceRegistry; - - const syncBuffersFromState = () => { - const state = store.getState(); - for (const sourceKey of Object.keys(state.sources)) { - if (!sourceBuffers.has(sourceKey)) { - sourceBuffers.set(sourceKey, new PlotBuffer(state.plot.maxPoints)); - } - sourceBuffers.get(sourceKey).maxPoints = state.plot.maxPoints; - } - - for (const sourceKey of Array.from(sourceBuffers.keys())) { - if (!state.sources[sourceKey]) { - sourceBuffers.delete(sourceKey); - } - } - }; - - const clearSourceBuffer = (sourceKey) => { - sourceBuffers.get(sourceKey)?.clear(); - }; - - const getGraphPoints = (state, graphId) => { - const graphConfig = state.graphs[graphId]; - const sourceBuffer = sourceBuffers.get(graphConfig.sourceKey); - const basePoints = sourceBuffer - ? sourceBuffer.getVisiblePoints(state.time.plotTimeMs, state.plot.windowDurationMs) - : []; - const transformedPoints = transformPoints(basePoints, graphConfig.transform); - return { - graphConfig, - points: transformedPoints, - range: deriveValueRange(transformedPoints, state.plot.valueRange), - }; - }; - - const actions = { - togglePause: () => timeController.togglePause(), - setSpeed: (speed) => timeController.setSpeed(speed), - resetScene: () => { - timeController.reset(); - sourceBuffers.forEach((plotBuffer) => plotBuffer.clear()); - sourceRegistry.reset(); - }, - togglePanel: (panelId) => { - store.setState((state) => ({ - ...state, - panels: { - ...state.panels, - [panelId]: { - ...state.panels[panelId], - visible: !state.panels[panelId].visible, - }, - }, - })); - }, - updateSource: (sourceKey, field, value) => { - store.setState((state) => ({ - ...state, - sources: { - ...state.sources, - [sourceKey]: { - ...state.sources[sourceKey], - [field]: value, - ...(field === 'type' - ? { - loadError: value === 'csv-replay' && state.sources[sourceKey].dataset.length === 0 - ? (state.sources[sourceKey].dataFileName - ? `Reload ${state.sources[sourceKey].dataFileName} to restore replay data` - : 'Load a CSV file to begin replay') - : '', - wsStatus: value === 'websocket' ? state.sources[sourceKey].wsStatus : 'idle', - wsStatusDetail: value === 'websocket' ? state.sources[sourceKey].wsStatusDetail : '', - } - : {}), - }, - }, - })); - sourceRegistry.syncFromState(); - syncBuffersFromState(); - - if (field === 'type' || field === 'wsUrl' || field === 'wsReconnectMs') { - clearSourceBuffer(sourceKey); - sourceRegistry.reset(); - } - }, - loadSourceFile: async (sourceKey, file) => { - try { - const state = store.getState(); - const sampleRateHz = state.sources[sourceKey]?.sampleRateHz ?? 60; - const text = await file.text(); - const { points, metadata } = parseReplayCsv(text, { sampleRateHz }); - - clearSourceBuffer(sourceKey); - store.setState((currentState) => ({ - ...currentState, - sources: { - ...currentState.sources, - [sourceKey]: { - ...currentState.sources[sourceKey], - type: 'csv-replay', - dataset: points, - dataFileName: file.name, - datasetPointCount: metadata.pointCount, - datasetDurationMs: metadata.durationMs, - loadError: '', - wsStatus: 'idle', - wsStatusDetail: '', - }, - }, - })); - sourceRegistry.syncFromState(); - sourceRegistry.reset(); - } catch (error) { - store.setState((currentState) => ({ - ...currentState, - sources: { - ...currentState.sources, - [sourceKey]: { - ...currentState.sources[sourceKey], - loadError: error instanceof Error ? error.message : String(error), - }, - }, - })); - } - }, - updatePlot: (field, value) => { - store.setState((state) => ({ - ...state, - plot: { - ...state.plot, - [field]: value, - }, - })); - - if (field === 'maxPoints') { - buffer.maxPoints = clamp(value, 200, 4000); - sourceBuffers.forEach((plotBuffer) => { - plotBuffer.maxPoints = clamp(value, 200, 4000); - }); - } - }, - updateGraph: (graphId, field, value) => { - store.setState((state) => ({ - ...state, - graphs: { - ...state.graphs, - [graphId]: { - ...state.graphs[graphId], - [field]: value, - }, - }, - })); - }, - }; - - const panelManager = new PanelManager({ root, store, actions }); - const elements = panelManager.mount(); - - const plotView = new TimeplotView({ - host: elements.primaryCanvasHost, - panelId: 'primary', - title: 'Primary signal', - subtitle: null, - showReadouts: true, - lineColor: 0x9fd1ff, - pointColor: 0xe7f2ff, - }); - - const secondaryPlotView = new TimeplotView({ - host: elements.secondaryCanvasHost, - panelId: 'secondary', - title: 'Secondary signal', - subtitle: null, - showReadouts: false, - lineColor: 0xffc46b, - pointColor: 0xffe1b0, - }); - - const renderer = await plotView.init(); - await secondaryPlotView.init(); - store.patch({ - app: { - ...store.getState().app, - renderer, - }, - }); - - sourceRegistry = new SourceRegistry(store, bus); - - bus.on('data:point', (point) => { - sourceBuffers.get(point.sourceId)?.addPoint(point); - }); - - const keyHandler = (event) => { - if (event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement) { - return; - } - - if (event.code === 'Space') { - event.preventDefault(); - actions.togglePause(); - return; - } - - if (event.key === '[') { - actions.setSpeed(store.getState().time.speed - 0.1); - return; - } - - if (event.key === ']') { - actions.setSpeed(store.getState().time.speed + 0.1); - return; - } - - if (event.key.toLowerCase() === 'g') { - actions.updatePlot('showGrid', !store.getState().plot.showGrid); - } - }; - - window.addEventListener('keydown', keyHandler); - - plotView.app.ticker.add(() => { - timeController.tick(); - sourceRegistry.syncFromState(); - syncBuffersFromState(); - sourceRegistry.update(store.getState().time.plotTimeMs); - - const state = store.getState(); - const primaryGraph = getGraphPoints(state, 'primary'); - const secondaryGraph = getGraphPoints(state, 'secondary'); - - plotView.panelTitle = state.graphs.primary.title; - plotView.panelSubtitle = `${state.sources[state.graphs.primary.sourceKey].label} · ${describeTransform(state.graphs.primary.transform)} · time ↓`; - secondaryPlotView.panelTitle = state.graphs.secondary.title; - secondaryPlotView.panelSubtitle = `${state.sources[state.graphs.secondary.sourceKey].label} · ${describeTransform(state.graphs.secondary.transform)} · time ↓`; - - const primaryState = { - ...state, - plot: { - ...state.plot, - valueRange: primaryGraph.range, - }, - }; - - const secondaryState = { - ...state, - plot: { - ...state.plot, - valueRange: secondaryGraph.range, - }, - }; - - plotView.render(primaryState, primaryGraph.points); - secondaryPlotView.render(secondaryState, secondaryGraph.points); - - const primaryHover = plotView.getHoverCandidate(); - const secondaryHover = secondaryPlotView.getHoverCandidate(); - const activeHover = pickActiveHover(primaryHover, secondaryHover); - - if (!activeHover) { - plotView.clearHover(); - secondaryPlotView.clearHover(); - store.setState((currentState) => ({ - ...currentState, - plot: { - ...currentState.plot, - hoveredPoint: null, - tooltip: { - ...currentState.plot.tooltip, - visible: false, - point: null, - linkedPoint: null, - }, - }, - })); - panelManager.sync(store.getState(), { - primary: primaryGraph.points.length, - secondary: secondaryGraph.points.length, - }); - return; - } - - const primaryLinkedPoint = plotView.findNearestScreenPointByTime(activeHover.point.timeMs); - const secondaryLinkedPoint = secondaryPlotView.findNearestScreenPointByTime(activeHover.point.timeMs); - - plotView.renderLinkedHover(primaryLinkedPoint); - secondaryPlotView.renderLinkedHover(secondaryLinkedPoint); - - const activePanelLabel = activeHover.panelId === 'secondary' - ? state.graphs.secondary.title - : state.graphs.primary.title; - const linkedPoint = activeHover.panelId === 'secondary' ? primaryLinkedPoint : secondaryLinkedPoint; - const linkedPanelLabel = activeHover.panelId === 'secondary' - ? state.graphs.primary.title - : state.graphs.secondary.title; - - store.setState((currentState) => ({ - ...currentState, - plot: { - ...currentState.plot, - hoveredPoint: activeHover.point, - tooltip: { - ...currentState.plot.tooltip, - visible: true, - panelId: activeHover.panelId, - panelLabel: activePanelLabel, - x: activeHover.x, - y: activeHover.y, - point: activeHover.point, - linkedPoint, - linkedPanelLabel, - }, - }, - })); - - panelManager.sync(store.getState(), { - primary: primaryGraph.points.length, - secondary: secondaryGraph.points.length, - }); - }); - - return { - destroy() { - window.removeEventListener('keydown', keyHandler); - plotView.destroy(); - secondaryPlotView.destroy(); - }, - }; -} diff --git a/web-timeplot/src/bootstrap.js b/web-timeplot/src/bootstrap.js deleted file mode 100644 index 4b073bc..0000000 --- a/web-timeplot/src/bootstrap.js +++ /dev/null @@ -1,18 +0,0 @@ -import './styles.css'; -import { createApp } from './app/create-app.js'; - -const root = document.getElementById('app'); - -if (!root) { - throw new Error('App root not found'); -} - -createApp(root).catch((error) => { - console.error('Failed to start TimePlot', error); - root.innerHTML = ` -
-

TimePlot failed to start

-
${String(error)}
-
- `; -}); diff --git a/web-timeplot/src/core/event-bus.js b/web-timeplot/src/core/event-bus.js deleted file mode 100644 index 192eb6d..0000000 --- a/web-timeplot/src/core/event-bus.js +++ /dev/null @@ -1,32 +0,0 @@ -export class EventBus { - constructor() { - this.listeners = new Map(); - } - - on(eventName, listener) { - if (!this.listeners.has(eventName)) { - this.listeners.set(eventName, new Set()); - } - - const listeners = this.listeners.get(eventName); - listeners.add(listener); - - return () => { - listeners.delete(listener); - if (listeners.size === 0) { - this.listeners.delete(eventName); - } - }; - } - - emit(eventName, payload) { - const listeners = this.listeners.get(eventName); - if (!listeners) { - return; - } - - for (const listener of listeners) { - listener(payload); - } - } -} diff --git a/web-timeplot/src/core/store.js b/web-timeplot/src/core/store.js deleted file mode 100644 index 38052eb..0000000 --- a/web-timeplot/src/core/store.js +++ /dev/null @@ -1,291 +0,0 @@ -const STORAGE_KEY = 'timeplot.app-state.v1'; - -function clonePanelState(panels) { - return Object.fromEntries(Object.entries(panels).map(([key, value]) => [key, { ...value }])); -} - -function cloneNamedState(items) { - return Object.fromEntries(Object.entries(items).map(([key, value]) => [key, { ...value }])); -} - -function sanitizePersistedSource(source) { - return { - type: source.type, - preset: source.preset, - sampleRateHz: source.sampleRateHz, - amplitude: source.amplitude, - noise: source.noise, - replayRate: source.replayRate, - dataFileName: source.dataFileName, - wsUrl: source.wsUrl, - wsReconnectMs: source.wsReconnectMs, - }; -} - -function createPersistableState(state) { - return { - plot: { - showGrid: state.plot.showGrid, - showPoints: state.plot.showPoints, - windowDurationMs: state.plot.windowDurationMs, - maxPoints: state.plot.maxPoints, - }, - time: { - speed: state.time.speed, - }, - panels: clonePanelState(state.panels), - graphs: cloneNamedState(state.graphs), - sources: Object.fromEntries(Object.entries(state.sources).map(([key, value]) => [ - key, - sanitizePersistedSource(value), - ])), - }; -} - -function mergePersistedState(baseState, persistedState) { - if (!persistedState || typeof persistedState !== 'object') { - return baseState; - } - - const mergedState = { - ...baseState, - time: persistedState.time - ? { - ...baseState.time, - speed: persistedState.time.speed ?? baseState.time.speed, - paused: false, - } - : baseState.time, - plot: persistedState.plot - ? { - ...baseState.plot, - ...persistedState.plot, - valueRange: baseState.plot.valueRange, - hoveredPoint: null, - tooltip: { ...baseState.plot.tooltip }, - } - : baseState.plot, - panels: persistedState.panels - ? clonePanelState(Object.fromEntries(Object.entries(baseState.panels).map(([key, value]) => [ - key, - { - ...value, - ...(persistedState.panels[key] ?? {}), - }, - ]))) - : baseState.panels, - graphs: persistedState.graphs - ? cloneNamedState(Object.fromEntries(Object.entries(baseState.graphs).map(([key, value]) => [ - key, - { - ...value, - ...(persistedState.graphs[key] ?? {}), - }, - ]))) - : baseState.graphs, - sources: persistedState.sources - ? Object.fromEntries(Object.entries(baseState.sources).map(([key, value]) => { - const persistedSource = persistedState.sources[key] ?? {}; - const nextType = persistedSource.type ?? value.type; - - return [ - key, - { - ...value, - ...persistedSource, - type: nextType, - dataset: [], - datasetPointCount: 0, - datasetDurationMs: 0, - loadError: nextType === 'csv-replay' && persistedSource.dataFileName - ? `Reload ${persistedSource.dataFileName} to restore replay data` - : '', - wsStatus: 'idle', - wsStatusDetail: '', - }, - ]; - })) - : baseState.sources, - }; - - return mergedState; -} - -function loadPersistedState() { - if (typeof localStorage === 'undefined') { - return null; - } - - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) { - return null; - } - - return JSON.parse(raw); - } catch (error) { - console.warn('[timeplot] failed to load persisted state', error); - return null; - } -} - -function savePersistedState(state) { - if (typeof localStorage === 'undefined') { - return; - } - - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(createPersistableState(state))); - } catch (error) { - console.warn('[timeplot] failed to persist state', error); - } -} - -export function createInitialState() { - return { - app: { - title: 'TimePlot', - renderer: 'pending', - }, - time: { - realNowMs: Date.now(), - realElapsedMs: 0, - plotTimeMs: 0, - speed: 1, - paused: false, - }, - plot: { - showGrid: true, - showPoints: true, - windowDurationMs: 20000, - maxPoints: 1600, - valueRange: { - min: -1.6, - max: 1.6, - }, - hoveredPoint: null, - tooltip: { - visible: false, - x: 0, - y: 0, - point: null, - }, - }, - sources: { - signalA: { - id: 'signal-a', - label: 'Signal A', - type: 'synthetic-wave', - preset: 'telemetry', - sampleRateHz: 60, - amplitude: 1, - noise: 0.08, - replayRate: 1, - dataset: [], - dataFileName: '', - datasetPointCount: 0, - datasetDurationMs: 0, - loadError: '', - wsUrl: 'ws://localhost:8080', - wsReconnectMs: 2000, - wsStatus: 'idle', - wsStatusDetail: '', - }, - signalB: { - id: 'signal-b', - label: 'Signal B', - type: 'synthetic-wave', - preset: 'chirp', - sampleRateHz: 48, - amplitude: 0.8, - noise: 0.04, - replayRate: 1, - dataset: [], - dataFileName: '', - datasetPointCount: 0, - datasetDurationMs: 0, - loadError: '', - wsUrl: 'ws://localhost:8080', - wsReconnectMs: 2000, - wsStatus: 'idle', - wsStatusDetail: '', - }, - }, - graphs: { - primary: { - sourceKey: 'signalA', - transform: 'raw', - title: 'Primary signal', - }, - secondary: { - sourceKey: 'signalB', - transform: 'delta', - title: 'Secondary signal', - }, - }, - panels: { - status: { title: 'Status', visible: true }, - source: { title: 'Data Source', visible: true }, - config: { title: 'Config', visible: true }, - help: { title: 'Help', visible: false }, - }, - }; -} - -export class Store { - constructor(initialState = createInitialState()) { - this.state = mergePersistedState(initialState, loadPersistedState()); - this.listeners = new Set(); - } - - getState() { - return this.state; - } - - subscribe(listener) { - this.listeners.add(listener); - return () => this.listeners.delete(listener); - } - - setState(updater) { - const nextState = typeof updater === 'function' ? updater(this.state) : updater; - this.state = nextState; - savePersistedState(this.state); - for (const listener of this.listeners) { - listener(this.state); - } - } - - patch(partial) { - this.setState((state) => ({ - ...state, - ...partial, - time: partial.time ? { ...state.time, ...partial.time } : state.time, - plot: partial.plot - ? { - ...state.plot, - ...partial.plot, - valueRange: partial.plot.valueRange - ? { ...state.plot.valueRange, ...partial.plot.valueRange } - : state.plot.valueRange, - tooltip: partial.plot.tooltip - ? { ...state.plot.tooltip, ...partial.plot.tooltip } - : state.plot.tooltip, - } - : state.plot, - sources: partial.sources - ? Object.fromEntries(Object.entries({ ...state.sources, ...partial.sources }).map(([key, value]) => [ - key, - { ...state.sources[key], ...value }, - ])) - : state.sources, - graphs: partial.graphs - ? cloneNamedState(Object.fromEntries(Object.entries({ ...state.graphs, ...partial.graphs }).map(([key, value]) => [ - key, - { ...state.graphs[key], ...value }, - ]))) - : state.graphs, - panels: partial.panels ? clonePanelState({ ...state.panels, ...partial.panels }) : state.panels, - })); - } -} diff --git a/web-timeplot/src/core/time-controller.js b/web-timeplot/src/core/time-controller.js deleted file mode 100644 index 7cd57c7..0000000 --- a/web-timeplot/src/core/time-controller.js +++ /dev/null @@ -1,80 +0,0 @@ -export class TimeController { - constructor(store) { - this.store = store; - this.lastFrameTime = performance.now(); - } - - tick(now = performance.now()) { - const deltaMs = now - this.lastFrameTime; - this.lastFrameTime = now; - - this.store.setState((state) => { - const realElapsedMs = state.time.realElapsedMs + deltaMs; - const plotDeltaMs = state.time.paused ? 0 : deltaMs * state.time.speed; - - return { - ...state, - time: { - ...state.time, - realNowMs: Date.now(), - realElapsedMs, - plotTimeMs: Math.max(0, state.time.plotTimeMs + plotDeltaMs), - }, - }; - }); - - return deltaMs; - } - - togglePause() { - this.store.setState((state) => ({ - ...state, - time: { - ...state.time, - paused: !state.time.paused, - }, - })); - } - - setPaused(paused) { - this.store.setState((state) => ({ - ...state, - time: { - ...state.time, - paused, - }, - })); - } - - setSpeed(speed) { - const clampedSpeed = Math.max(0.1, Math.min(12, speed)); - this.store.setState((state) => ({ - ...state, - time: { - ...state.time, - speed: clampedSpeed, - }, - })); - } - - reset() { - this.store.setState((state) => ({ - ...state, - time: { - ...state.time, - realElapsedMs: 0, - plotTimeMs: 0, - }, - plot: { - ...state.plot, - hoveredPoint: null, - tooltip: { - ...state.plot.tooltip, - visible: false, - point: null, - }, - }, - })); - this.lastFrameTime = performance.now(); - } -} diff --git a/web-timeplot/src/data-sources.js b/web-timeplot/src/data-sources.js deleted file mode 100644 index 749a151..0000000 --- a/web-timeplot/src/data-sources.js +++ /dev/null @@ -1,517 +0,0 @@ -/** - * Data Sources - Components that generate or provide data to plots - * - * This module implements the data provider side of the architecture. - * Data sources know how to generate or fetch data, but don't know - * anything about visualization. - * - * Architecture: - * - DataSource: Base class with event emitting - * - Specific sources: Implement different data generation strategies - * - Connection: Links sources to plots (see plot-connections.js) - */ - -// Simple EventEmitter (same as in state.js, could be extracted to utils) -class EventEmitter { - constructor() { - this.events = new Map(); - } - - on(event, callback) { - if (!this.events.has(event)) { - this.events.set(event, []); - } - this.events.get(event).push(callback); - return () => this.off(event, callback); - } - - off(event, callback) { - if (!this.events.has(event)) return; - const callbacks = this.events.get(event); - const index = callbacks.indexOf(callback); - if (index > -1) { - callbacks.splice(index, 1); - } - } - - emit(event, data) { - if (!this.events.has(event)) return; - this.events.get(event).forEach(callback => { - try { - callback(data); - } catch (e) { - console.error(`[DataSource] Error in event handler for '${event}':`, e); - } - }); - } -} - -/** - * Base class for all data sources - * - * Events emitted: - * - 'line': {points: Array, timestamp: number, metadata: Object} - * - 'point': {value: number, timestamp: number} - * - 'error': {error: Error} - */ -export class DataSource extends EventEmitter { - constructor(config = {}) { - super(); - this.config = config; - this.isRunning = false; - this.time = 0; - } - - /** - * Start generating/providing data - */ - start() { - this.isRunning = true; - } - - /** - * Stop generating/providing data - */ - stop() { - this.isRunning = false; - } - - /** - * Reset the data source to initial state - */ - reset() { - this.time = 0; - } - - /** - * Emit a complete line of data - */ - emitLine(points, metadata = {}) { - this.emit('line', { - points, - timestamp: metadata.timestamp || Date.now(), - metadata, - }); - } - - /** - * Emit a single data point - */ - emitPoint(value, timestamp = Date.now()) { - this.emit('point', { - value, - timestamp, - }); - } - - /** - * Emit an error - */ - emitError(error) { - this.emit('error', { error }); - } -} - -/** - * Synthetic data source using test generators - * Uses the generators from test-data-generators.js - */ -export class SyntheticDataSource extends DataSource { - constructor(config = {}) { - super(config); - this.generator = config.generator; // Instance of DataGenerator - this.pointsPerLine = config.pointsPerLine || 100; - this.width = config.width || 800; - this.lineInterval = config.lineInterval || 100; // ms between lines - this.intervalHandle = null; - } - - start() { - if (this.isRunning) return; - super.start(); - - // Generate a new line periodically - this.intervalHandle = setInterval(() => { - this.generateAndEmitLine(); - }, this.lineInterval); - - // Generate initial line immediately - this.generateAndEmitLine(); - } - - stop() { - super.stop(); - if (this.intervalHandle) { - clearInterval(this.intervalHandle); - this.intervalHandle = null; - } - } - - generateAndEmitLine() { - if (!this.generator) { - this.emitError(new Error('No generator configured')); - return; - } - - const points = this.generator.generateLine(this.pointsPerLine, this.width); - this.emitLine(points, { - timestamp: Date.now(), - generatorType: this.generator.constructor.name, - }); - } - - setGenerator(generator) { - this.generator = generator; - } -} - -/** - * Function-based data source - * Evaluates a user-provided function to generate data - */ -export class FunctionDataSource extends DataSource { - constructor(config = {}) { - super(config); - // Function should have signature: (x, t) => y - // x: normalized position 0-1 - // t: time in seconds - // returns: y value - this.func = config.func || ((x, t) => Math.sin(x * 10 + t)); - this.pointsPerLine = config.pointsPerLine || 100; - this.width = config.width || 800; - this.amplitude = config.amplitude || 30; - this.lineInterval = config.lineInterval || 100; - this.intervalHandle = null; - } - - start() { - if (this.isRunning) return; - super.start(); - - this.intervalHandle = setInterval(() => { - this.generateAndEmitLine(); - }, this.lineInterval); - - this.generateAndEmitLine(); - } - - stop() { - super.stop(); - if (this.intervalHandle) { - clearInterval(this.intervalHandle); - this.intervalHandle = null; - } - } - - generateAndEmitLine() { - const points = []; - const t = this.time; - - for (let i = 0; i < this.pointsPerLine; i++) { - const x = (i / this.pointsPerLine) * this.width; - const normalizedX = i / this.pointsPerLine; - const y = this.func(normalizedX, t) * this.amplitude; - points.push({ x, y }); - } - - this.emitLine(points, { - timestamp: Date.now(), - time: t, - }); - - this.time += this.lineInterval / 1000; - } - - setFunction(func) { - this.func = func; - } -} - -/** - * Streaming data source - * Emits individual data points that get buffered into lines - */ -export class StreamingDataSource extends DataSource { - constructor(config = {}) { - super(config); - this.generator = config.generator; - this.sampleRate = config.sampleRate || 60; // Samples per second - this.intervalHandle = null; - } - - start() { - if (this.isRunning) return; - super.start(); - - const intervalMs = 1000 / this.sampleRate; - this.intervalHandle = setInterval(() => { - this.generateAndEmitPoint(); - }, intervalMs); - } - - stop() { - super.stop(); - if (this.intervalHandle) { - clearInterval(this.intervalHandle); - this.intervalHandle = null; - } - } - - generateAndEmitPoint() { - if (!this.generator) { - this.emitError(new Error('No generator configured')); - return; - } - - const value = this.generator.sample(); - this.generator.time += 1 / this.generator.sampleRate; - this.emitPoint(value, Date.now()); - } - - setGenerator(generator) { - this.generator = generator; - } -} - -/** - * WebSocket data source (for real data) - * Receives data from a WebSocket connection - */ -export class WebSocketDataSource extends DataSource { - constructor(config = {}) { - super(config); - this.url = config.url; - this.socket = null; - this.reconnectInterval = config.reconnectInterval || 5000; - this.reconnectHandle = null; - } - - start() { - if (this.isRunning) return; - super.start(); - this.connect(); - } - - stop() { - super.stop(); - if (this.socket) { - this.socket.close(); - this.socket = null; - } - if (this.reconnectHandle) { - clearTimeout(this.reconnectHandle); - this.reconnectHandle = null; - } - } - - connect() { - try { - this.socket = new WebSocket(this.url); - - this.socket.onopen = () => { - console.log(`[WebSocketDataSource] Connected to ${this.url}`); - }; - - this.socket.onmessage = (event) => { - this.handleMessage(event.data); - }; - - this.socket.onerror = (error) => { - console.error('[WebSocketDataSource] Error:', error); - this.emitError(error); - }; - - this.socket.onclose = () => { - console.log('[WebSocketDataSource] Connection closed'); - if (this.isRunning) { - // Auto-reconnect - this.reconnectHandle = setTimeout(() => { - this.connect(); - }, this.reconnectInterval); - } - }; - } catch (error) { - console.error('[WebSocketDataSource] Failed to connect:', error); - this.emitError(error); - } - } - - handleMessage(data) { - try { - const parsed = JSON.parse(data); - - // Expect format: {type: 'line', points: [...]} or {type: 'point', value: ...} - if (parsed.type === 'line' && parsed.points) { - this.emitLine(parsed.points, parsed.metadata || {}); - } else if (parsed.type === 'point' && parsed.value !== undefined) { - this.emitPoint(parsed.value, parsed.timestamp); - } else { - console.warn('[WebSocketDataSource] Unknown message format:', parsed); - } - } catch (error) { - console.error('[WebSocketDataSource] Failed to parse message:', error); - this.emitError(error); - } - } - - send(data) { - if (this.socket && this.socket.readyState === WebSocket.OPEN) { - this.socket.send(JSON.stringify(data)); - } - } -} - -/** - * CSV File data source - * Reads data from CSV files (for replay/analysis) - */ -export class CSVDataSource extends DataSource { - constructor(config = {}) { - super(config); - this.data = []; // Parsed CSV data - this.currentIndex = 0; - this.playbackRate = config.playbackRate || 1.0; - this.loop = config.loop || false; - this.intervalHandle = null; - } - - /** - * Load CSV data from a string - * Expected format: timestamp,value or x,y format - */ - loadCSV(csvString) { - const lines = csvString.trim().split('\n'); - const headers = lines[0].split(',').map(h => h.trim()); - - this.data = []; - for (let i = 1; i < lines.length; i++) { - const values = lines[i].split(',').map(v => parseFloat(v.trim())); - if (values.length >= 2 && !values.some(isNaN)) { - this.data.push({ - timestamp: values[0], - value: values[1], - }); - } - } - - console.log(`[CSVDataSource] Loaded ${this.data.length} data points`); - } - - start() { - if (this.isRunning || this.data.length === 0) return; - super.start(); - - // Play back at specified rate - this.intervalHandle = setInterval(() => { - this.emitNextPoint(); - }, 16 / this.playbackRate); // ~60fps adjusted by playback rate - } - - stop() { - super.stop(); - if (this.intervalHandle) { - clearInterval(this.intervalHandle); - this.intervalHandle = null; - } - } - - reset() { - super.reset(); - this.currentIndex = 0; - } - - emitNextPoint() { - if (this.currentIndex >= this.data.length) { - if (this.loop) { - this.currentIndex = 0; - } else { - this.stop(); - return; - } - } - - const point = this.data[this.currentIndex]; - this.emitPoint(point.value, point.timestamp); - this.currentIndex++; - } -} - -/** - * Multi-source combiner - * Combines data from multiple sources - */ -export class CompositeDataSource extends DataSource { - constructor(config = {}) { - super(config); - this.sources = config.sources || []; - this.combineMode = config.combineMode || 'average'; // 'average', 'sum', 'max', 'min' - this.pointBuffer = new Map(); // sourceId => latest point - } - - start() { - if (this.isRunning) return; - super.start(); - - // Subscribe to all sources - this.sources.forEach((source, idx) => { - source.on('point', (data) => { - this.handleSourcePoint(idx, data); - }); - source.on('line', (data) => { - this.handleSourceLine(idx, data); - }); - source.start(); - }); - } - - stop() { - super.stop(); - this.sources.forEach(source => source.stop()); - } - - handleSourcePoint(sourceIdx, data) { - this.pointBuffer.set(sourceIdx, data.value); - - // If we have data from all sources, combine and emit - if (this.pointBuffer.size === this.sources.length) { - const combined = this.combineValues(Array.from(this.pointBuffer.values())); - this.emitPoint(combined, data.timestamp); - } - } - - handleSourceLine(sourceIdx, data) { - // For lines, just pass through for now - // Could implement line combination if needed - this.emitLine(data.points, data.metadata); - } - - combineValues(values) { - switch (this.combineMode) { - case 'sum': - return values.reduce((a, b) => a + b, 0); - case 'average': - return values.reduce((a, b) => a + b, 0) / values.length; - case 'max': - return Math.max(...values); - case 'min': - return Math.min(...values); - default: - return values[0]; - } - } - - addSource(source) { - this.sources.push(source); - if (this.isRunning) { - source.start(); - } - } - - removeSource(source) { - const idx = this.sources.indexOf(source); - if (idx > -1) { - source.stop(); - this.sources.splice(idx, 1); - } - } -} diff --git a/web-timeplot/src/data/base-source.js b/web-timeplot/src/data/base-source.js deleted file mode 100644 index 55dbdc3..0000000 --- a/web-timeplot/src/data/base-source.js +++ /dev/null @@ -1,21 +0,0 @@ -export class BaseSource { - constructor(config = {}) { - this.config = { ...config }; - this.running = false; - } - - start() { - this.running = true; - } - - stop() { - this.running = false; - } - - updateConfig(nextConfig) { - this.config = { - ...this.config, - ...nextConfig, - }; - } -} diff --git a/web-timeplot/src/data/csv-replay-source.js b/web-timeplot/src/data/csv-replay-source.js deleted file mode 100644 index c4e6a66..0000000 --- a/web-timeplot/src/data/csv-replay-source.js +++ /dev/null @@ -1,60 +0,0 @@ -import { BaseSource } from './base-source.js'; - -function clamp(value, min, max) { - return Math.min(max, Math.max(min, value)); -} - -export class CsvReplaySource extends BaseSource { - constructor(config = {}) { - super({ - replayRate: 1, - dataset: [], - ...config, - }); - this.sourceType = 'csv-replay'; - this.nextPointIndex = 0; - } - - start(startTimeMs = 0) { - super.start(); - this.reset(startTimeMs); - } - - reset() { - this.nextPointIndex = 0; - } - - updateConfig(nextConfig) { - const datasetChanged = nextConfig.dataset !== this.config.dataset; - super.updateConfig(nextConfig); - if (datasetChanged) { - this.reset(); - } - } - - update(currentPlotTimeMs) { - if (!this.running || !Array.isArray(this.config.dataset) || this.config.dataset.length === 0) { - return []; - } - - const replayRate = clamp(this.config.replayRate ?? 1, 0.1, 8); - const targetDatasetTimeMs = currentPlotTimeMs * replayRate; - const points = []; - - while (this.nextPointIndex < this.config.dataset.length) { - const datasetPoint = this.config.dataset[this.nextPointIndex]; - if (datasetPoint.timeMs > targetDatasetTimeMs) { - break; - } - - points.push({ - timeMs: datasetPoint.timeMs / replayRate, - value: datasetPoint.value, - sourceId: this.config.id ?? 'csv-replay', - }); - this.nextPointIndex += 1; - } - - return points; - } -} diff --git a/web-timeplot/src/data/parse-replay-csv.js b/web-timeplot/src/data/parse-replay-csv.js deleted file mode 100644 index b6ce97a..0000000 --- a/web-timeplot/src/data/parse-replay-csv.js +++ /dev/null @@ -1,108 +0,0 @@ -function splitRow(line) { - return line.split(/[;,\t]/).map((value) => value.trim()); -} - -function isNumeric(value) { - return value !== '' && Number.isFinite(Number(value)); -} - -function detectHeader(rows) { - if (rows.length === 0) { - return { hasHeader: false, headers: [] }; - } - - const [firstRow] = rows; - const hasHeader = firstRow.some((value) => !isNumeric(value)); - return { - hasHeader, - headers: hasHeader ? firstRow.map((value) => value.toLowerCase()) : [], - }; -} - -function detectTimeScale(headers) { - const timeHeader = headers.find((header) => header.includes('time') || header.includes('timestamp')); - if (!timeHeader) { - return 1; - } - - if (timeHeader.includes('sec') && !timeHeader.includes('msec') && !timeHeader.includes('ms')) { - return 1000; - } - - return 1; -} - -function detectColumnIndexes(headers, columnCount) { - if (headers.length === 0) { - return { - timeIndex: columnCount > 1 ? 0 : -1, - valueIndex: columnCount > 1 ? 1 : 0, - }; - } - - const timeIndex = headers.findIndex((header) => header.includes('time') || header.includes('timestamp')); - const valueIndex = headers.findIndex((header) => header.includes('value') || header.includes('signal') || header.includes('y')); - - return { - timeIndex, - valueIndex: valueIndex >= 0 ? valueIndex : (headers.length > 1 ? 1 : 0), - }; -} - -export function parseReplayCsv(text, { sampleRateHz = 60 } = {}) { - const rows = text - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith('#')) - .map(splitRow) - .filter((row) => row.some((value) => value !== '')); - - if (rows.length === 0) { - throw new Error('CSV file is empty'); - } - - const { hasHeader, headers } = detectHeader(rows); - const dataRows = hasHeader ? rows.slice(1) : rows; - const columnCount = rows[0].length; - const { timeIndex, valueIndex } = detectColumnIndexes(headers, columnCount); - const timeScale = detectTimeScale(headers); - const intervalMs = 1000 / Math.max(1, sampleRateHz); - - const points = dataRows - .map((row, index) => { - const rawValue = row[valueIndex]; - if (!isNumeric(rawValue)) { - return null; - } - - const parsedValue = Number(rawValue); - const parsedTime = timeIndex >= 0 && isNumeric(row[timeIndex]) - ? Number(row[timeIndex]) * timeScale - : index * intervalMs; - - return { - timeMs: parsedTime, - value: parsedValue, - }; - }) - .filter(Boolean) - .sort((left, right) => left.timeMs - right.timeMs); - - if (points.length === 0) { - throw new Error('CSV file did not contain any numeric data points'); - } - - const firstTime = points[0].timeMs; - const normalizedPoints = points.map((point) => ({ - timeMs: point.timeMs - firstTime, - value: point.value, - })); - - return { - points: normalizedPoints, - metadata: { - pointCount: normalizedPoints.length, - durationMs: normalizedPoints.at(-1)?.timeMs ?? 0, - }, - }; -} diff --git a/web-timeplot/src/data/source-registry.js b/web-timeplot/src/data/source-registry.js deleted file mode 100644 index 917d06b..0000000 --- a/web-timeplot/src/data/source-registry.js +++ /dev/null @@ -1,90 +0,0 @@ -import { CsvReplaySource } from './csv-replay-source.js'; -import { SyntheticWaveSource } from './synthetic-wave-source.js'; -import { WebSocketSource } from './websocket-source.js'; - -export class SourceRegistry { - constructor(store, bus) { - this.store = store; - this.bus = bus; - this.sources = new Map(); - this.syncFromState(); - } - - syncFromState() { - const state = this.store.getState(); - const sourceEntries = Object.entries(state.sources); - const activeKeys = new Set(sourceEntries.map(([sourceKey]) => sourceKey)); - - for (const [sourceKey, config] of sourceEntries) { - const existingSource = this.sources.get(sourceKey); - - if (!existingSource) { - const nextSource = this.createSource(sourceKey, config); - this.sources.set(sourceKey, nextSource); - nextSource.start(state.time.plotTimeMs); - continue; - } - - if (existingSource.sourceType !== config.type) { - existingSource.stop(); - const replacementSource = this.createSource(sourceKey, config); - this.sources.set(sourceKey, replacementSource); - replacementSource.start(state.time.plotTimeMs); - continue; - } - - existingSource.updateConfig(config); - } - - for (const [sourceKey, source] of this.sources.entries()) { - if (!activeKeys.has(sourceKey)) { - source.stop(); - this.sources.delete(sourceKey); - } - } - } - - createSource(sourceKey, config) { - switch (config.type) { - case 'csv-replay': - return new CsvReplaySource(config); - case 'websocket': - return new WebSocketSource(config, { - onStatusChange: (statusPatch) => { - this.store.setState((state) => ({ - ...state, - sources: { - ...state.sources, - [sourceKey]: { - ...state.sources[sourceKey], - ...statusPatch, - }, - }, - })); - }, - }); - case 'synthetic-wave': - default: - return new SyntheticWaveSource(config); - } - } - - update(currentPlotTimeMs) { - for (const [sourceKey, source] of this.sources.entries()) { - const points = source.update(currentPlotTimeMs); - for (const point of points) { - this.bus.emit('data:point', { - ...point, - sourceId: sourceKey, - }); - } - } - } - - reset() { - const startTimeMs = this.store.getState().time.plotTimeMs; - for (const source of this.sources.values()) { - source.reset(startTimeMs); - } - } -} diff --git a/web-timeplot/src/data/synthetic-wave-source.js b/web-timeplot/src/data/synthetic-wave-source.js deleted file mode 100644 index df53319..0000000 --- a/web-timeplot/src/data/synthetic-wave-source.js +++ /dev/null @@ -1,87 +0,0 @@ -import { BaseSource } from './base-source.js'; - -function clamp(value, min, max) { - return Math.min(max, Math.max(min, value)); -} - -function createDeterministicNoise(seed) { - const x = Math.sin(seed * 12.9898) * 43758.5453; - return x - Math.floor(x); -} - -export class SyntheticWaveSource extends BaseSource { - constructor(config = {}) { - super({ - sampleRateHz: 60, - preset: 'telemetry', - amplitude: 1, - noise: 0.08, - ...config, - }); - this.sourceType = 'synthetic-wave'; - this.lastEmittedPlotTimeMs = 0; - } - - start(startTimeMs = 0) { - super.start(); - this.lastEmittedPlotTimeMs = startTimeMs; - } - - stop() { - super.stop(); - } - - reset(startTimeMs = 0) { - this.lastEmittedPlotTimeMs = startTimeMs; - } - - sampleValue(timeMs) { - const seconds = timeMs / 1000; - const amplitude = this.config.amplitude; - const noise = this.config.noise; - const grain = (createDeterministicNoise(timeMs * 0.017) - 0.5) * 2 * noise; - - switch (this.config.preset) { - case 'chirp': { - const sweep = Math.sin(seconds * seconds * 1.4); - return amplitude * (0.7 * sweep + 0.3 * Math.sin(seconds * 7.5)) + grain; - } - case 'burst': { - const burstPhase = (seconds % 6) - 1.5; - const burst = Math.sin(seconds * 9.5) * Math.exp(-(burstPhase ** 2) * 0.8); - return amplitude * (0.45 * Math.sin(seconds * 2.1) + burst) + grain; - } - case 'telemetry': - default: { - const carrier = Math.sin(seconds * 2.2); - const secondary = 0.35 * Math.cos(seconds * 6.4 + Math.sin(seconds * 0.8)); - const envelope = 0.15 * Math.sin(seconds * 0.33); - return amplitude * (carrier + secondary + envelope) + grain; - } - } - } - - update(currentPlotTimeMs) { - if (!this.running) { - return []; - } - - const intervalMs = 1000 / clamp(this.config.sampleRateHz, 1, 240); - if (currentPlotTimeMs < this.lastEmittedPlotTimeMs) { - this.lastEmittedPlotTimeMs = currentPlotTimeMs; - return []; - } - - const points = []; - while (this.lastEmittedPlotTimeMs + intervalMs <= currentPlotTimeMs) { - this.lastEmittedPlotTimeMs += intervalMs; - points.push({ - timeMs: this.lastEmittedPlotTimeMs, - value: this.sampleValue(this.lastEmittedPlotTimeMs), - sourceId: 'synthetic-wave', - }); - } - - return points; - } -} diff --git a/web-timeplot/src/data/websocket-source.js b/web-timeplot/src/data/websocket-source.js deleted file mode 100644 index 5458fb9..0000000 --- a/web-timeplot/src/data/websocket-source.js +++ /dev/null @@ -1,224 +0,0 @@ -import { BaseSource } from './base-source.js'; - -function clamp(value, min, max) { - return Math.min(max, Math.max(min, value)); -} - -function isFiniteNumber(value) { - return typeof value === 'number' && Number.isFinite(value); -} - -function parsePayload(payload) { - if (Array.isArray(payload)) { - return payload.flatMap((item) => parsePayload(item)); - } - - if (isFiniteNumber(payload)) { - return [{ value: payload, timestampMs: null }]; - } - - if (typeof payload === 'string') { - const trimmed = payload.trim(); - if (!trimmed) { - return []; - } - - const numeric = Number(trimmed); - if (Number.isFinite(numeric)) { - return [{ value: numeric, timestampMs: null }]; - } - - try { - return parsePayload(JSON.parse(trimmed)); - } catch { - return []; - } - } - - if (payload && typeof payload === 'object') { - const candidateValue = [payload.value, payload.y, payload.signal, payload.data] - .find((value) => Number.isFinite(Number(value))); - - if (candidateValue === undefined) { - return []; - } - - const candidateTimestamp = [payload.timeMs, payload.timestampMs, payload.timestamp, payload.t] - .find((value) => Number.isFinite(Number(value))); - - return [{ - value: Number(candidateValue), - timestampMs: candidateTimestamp === undefined ? null : Number(candidateTimestamp), - }]; - } - - return []; -} - -export class WebSocketSource extends BaseSource { - constructor(config = {}, { onStatusChange } = {}) { - super({ - wsUrl: 'ws://localhost:8080', - wsReconnectMs: 2000, - ...config, - }); - this.sourceType = 'websocket'; - this.onStatusChange = onStatusChange; - this.socket = null; - this.queue = []; - this.lastPlotTimeMs = 0; - this.reconnectTimer = null; - this.shouldReconnect = false; - this.firstSourceTimestampMs = null; - this.basePlotTimeMs = 0; - } - - start(startTimeMs = 0) { - super.start(); - this.lastPlotTimeMs = startTimeMs; - this.basePlotTimeMs = startTimeMs; - this.shouldReconnect = true; - this.connect(); - } - - stop() { - super.stop(); - this.shouldReconnect = false; - this.clearReconnectTimer(); - if (this.socket) { - this.socket.close(); - this.socket = null; - } - this.setStatus('disconnected', 'socket closed'); - } - - reset(startTimeMs = 0) { - this.queue = []; - this.lastPlotTimeMs = startTimeMs; - this.basePlotTimeMs = startTimeMs; - this.firstSourceTimestampMs = null; - } - - updateConfig(nextConfig) { - const previousUrl = this.config.wsUrl; - const previousReconnectMs = this.config.wsReconnectMs; - super.updateConfig(nextConfig); - - if ((previousUrl !== this.config.wsUrl || previousReconnectMs !== this.config.wsReconnectMs) && this.running) { - this.reconnect(); - } - } - - update(currentPlotTimeMs) { - this.lastPlotTimeMs = currentPlotTimeMs; - - if (this.queue.length === 0) { - return []; - } - - const points = []; - while (this.queue.length > 0) { - const nextPoint = this.queue.shift(); - let timeMs = currentPlotTimeMs; - - if (isFiniteNumber(nextPoint.timestampMs)) { - if (this.firstSourceTimestampMs === null) { - this.firstSourceTimestampMs = nextPoint.timestampMs; - this.basePlotTimeMs = currentPlotTimeMs; - } - timeMs = this.basePlotTimeMs + (nextPoint.timestampMs - this.firstSourceTimestampMs); - } - - points.push({ - timeMs, - value: nextPoint.value, - sourceId: this.config.id ?? 'websocket', - }); - } - - return points; - } - - reconnect() { - if (!this.running) { - return; - } - - this.clearReconnectTimer(); - if (this.socket) { - this.socket.close(); - this.socket = null; - } - this.connect(); - } - - connect() { - const url = this.config.wsUrl?.trim(); - if (!url) { - this.setStatus('idle', 'enter a websocket url'); - return; - } - - this.clearReconnectTimer(); - this.setStatus('connecting', url); - - try { - this.socket = new WebSocket(url); - } catch (error) { - this.setStatus('error', error instanceof Error ? error.message : String(error)); - this.scheduleReconnect(); - return; - } - - this.socket.addEventListener('open', () => { - this.setStatus('connected', url); - }); - - this.socket.addEventListener('message', (event) => { - const parsedPoints = parsePayload(event.data); - if (parsedPoints.length === 0) { - return; - } - this.queue.push(...parsedPoints); - }); - - this.socket.addEventListener('error', () => { - this.setStatus('error', 'socket error'); - }); - - this.socket.addEventListener('close', () => { - this.socket = null; - if (!this.running) { - return; - } - this.setStatus('disconnected', 'retrying'); - this.scheduleReconnect(); - }); - } - - scheduleReconnect() { - if (!this.shouldReconnect || !this.running) { - return; - } - - const reconnectMs = clamp(Number(this.config.wsReconnectMs) || 2000, 250, 30000); - this.clearReconnectTimer(); - this.reconnectTimer = window.setTimeout(() => { - this.connect(); - }, reconnectMs); - } - - clearReconnectTimer() { - if (this.reconnectTimer !== null) { - window.clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } - } - - setStatus(status, detail = '') { - this.onStatusChange?.({ - wsStatus: status, - wsStatusDetail: detail, - }); - } -} diff --git a/web-timeplot/src/demos.js b/web-timeplot/src/demos.js deleted file mode 100644 index 1dd6785..0000000 --- a/web-timeplot/src/demos.js +++ /dev/null @@ -1,697 +0,0 @@ -/** - * Preloaded Graphics Demos - * - * Each demo exports: - * - name: Display name - * - description: Short description - * - setup(app, state): Called once to create objects - * - update(app, state, objects): Called every frame - * - cleanup(app, objects): Called when switching demos - */ - -// ============================================================================ -// DEMO 1: BOUNCING PARTICLES -// ============================================================================ - -export const bouncingParticles = { - name: "Bouncing Particles", - description: "Colorful particles bouncing around the screen", - - setup(app, state) { - const particles = []; - const colors = [0xff6b6b, 0x4ecdc4, 0x45b7d1, 0xf9ca24, 0x6c5ce7]; - - for (let i = 0; i < 50; i++) { - const particle = new PIXI.Graphics(); - const size = 5 + Math.random() * 10; - particle.circle(0, 0, size); - particle.fill(colors[Math.floor(Math.random() * colors.length)]); - - particle.x = Math.random() * app.screen.width; - particle.y = Math.random() * app.screen.height; - particle.vx = (Math.random() - 0.5) * 8; - particle.vy = (Math.random() - 0.5) * 8; - particle.size = size; - - app.stage.addChild(particle); - particles.push(particle); - } - - return { particles }; - }, - - update(app, state, objects) { - objects.particles.forEach(p => { - p.x += p.vx; - p.y += p.vy; - - // Bounce off edges - if (p.x < p.size || p.x > app.screen.width - p.size) p.vx *= -1; - if (p.y < p.size || p.y > app.screen.height - p.size) p.vy *= -1; - - // Clamp to screen - p.x = Math.max(p.size, Math.min(app.screen.width - p.size, p.x)); - p.y = Math.max(p.size, Math.min(app.screen.height - p.size, p.y)); - }); - }, - - cleanup(app, objects) { - objects.particles.forEach(p => p.destroy()); - } -}; - -// ============================================================================ -// DEMO 2: SPIROGRAPH -// ============================================================================ - -export const spirograph = { - name: "Spirograph", - description: "Mesmerizing geometric spiral patterns", - - setup(app, state) { - const graphics = new PIXI.Graphics(); - app.stage.addChild(graphics); - - return { - graphics, - angle: 0, - points: [] - }; - }, - - update(app, state, objects) { - const cx = app.screen.width / 2; - const cy = app.screen.height / 2; - const t = state.state.time.current; - - // Generate new point - const r1 = 150; - const r2 = 50; - const r3 = 30; - - const x = cx + Math.cos(t * 0.5) * r1 + Math.cos(t * 2) * r2 + Math.cos(t * 5) * r3; - const y = cy + Math.sin(t * 0.5) * r1 + Math.sin(t * 2) * r2 + Math.sin(t * 5) * r3; - - objects.points.push({ x, y }); - - // Keep only last 500 points - if (objects.points.length > 500) { - objects.points.shift(); - } - - // Draw trail - objects.graphics.clear(); - if (objects.points.length > 1) { - for (let i = 1; i < objects.points.length; i++) { - const alpha = i / objects.points.length; - const hue = (i / objects.points.length) * 360; - objects.graphics.moveTo(objects.points[i-1].x, objects.points[i-1].y); - objects.graphics.lineTo(objects.points[i].x, objects.points[i].y); - objects.graphics.stroke({ width: 2, color: hslToHex(hue, 100, 60), alpha }); - } - } - }, - - cleanup(app, objects) { - objects.graphics.destroy(); - } -}; - -// ============================================================================ -// DEMO 3: STARFIELD -// ============================================================================ - -export const starfield = { - name: "Starfield", - description: "Flying through space at warp speed", - - setup(app, state) { - const stars = []; - - for (let i = 0; i < 200; i++) { - const star = new PIXI.Graphics(); - star.circle(0, 0, 2); - star.fill(0xffffff); - - star.x = (Math.random() - 0.5) * app.screen.width * 2; - star.y = (Math.random() - 0.5) * app.screen.height * 2; - star.z = Math.random() * 1000; - - app.stage.addChild(star); - stars.push(star); - } - - return { stars }; - }, - - update(app, state, objects) { - const cx = app.screen.width / 2; - const cy = app.screen.height / 2; - const speed = 5; - - objects.stars.forEach(star => { - star.z -= speed; - - if (star.z <= 0) { - star.z = 1000; - star.x = (Math.random() - 0.5) * app.screen.width * 2; - star.y = (Math.random() - 0.5) * app.screen.height * 2; - } - - const screenX = cx + (star.x / star.z) * 200; - const screenY = cy + (star.y / star.z) * 200; - const size = (1 - star.z / 1000) * 4 + 1; - - star.x = star.x; - star.y = star.y; - star.position.set(screenX, screenY); - star.scale.set(size); - star.alpha = 1 - star.z / 1000; - }); - }, - - cleanup(app, objects) { - objects.stars.forEach(s => s.destroy()); - } -}; - -// ============================================================================ -// DEMO 4: WAVE INTERFERENCE -// ============================================================================ - -export const waveInterference = { - name: "Wave Interference", - description: "Rippling wave patterns", - - setup(app, state) { - const gridSize = 20; - const cols = Math.floor(app.screen.width / gridSize); - const rows = Math.floor(app.screen.height / gridSize); - const circles = []; - - for (let i = 0; i < cols; i++) { - for (let j = 0; j < rows; j++) { - const circle = new PIXI.Graphics(); - circle.circle(0, 0, 4); - circle.fill(0x4ecdc4); - circle.x = i * gridSize + gridSize / 2; - circle.y = j * gridSize + gridSize / 2; - circle.baseX = circle.x; - circle.baseY = circle.y; - - app.stage.addChild(circle); - circles.push(circle); - } - } - - return { circles, sources: [ - { x: app.screen.width * 0.3, y: app.screen.height * 0.5 }, - { x: app.screen.width * 0.7, y: app.screen.height * 0.5 } - ]}; - }, - - update(app, state, objects) { - const t = state.state.time.current; - - objects.circles.forEach(c => { - let totalOffset = 0; - - objects.sources.forEach(source => { - const dx = c.baseX - source.x; - const dy = c.baseY - source.y; - const dist = Math.sqrt(dx * dx + dy * dy); - totalOffset += Math.sin(dist * 0.05 - t * 3) * 10; - }); - - c.y = c.baseY + totalOffset; - c.alpha = 0.3 + (Math.sin(totalOffset * 0.1) + 1) * 0.35; - }); - }, - - cleanup(app, objects) { - objects.circles.forEach(c => c.destroy()); - } -}; - -// ============================================================================ -// DEMO 5: CIRCLE PACKING -// ============================================================================ - -export const circlePacking = { - name: "Circle Packing", - description: "Organic growth simulation", - - setup(app, state) { - const circles = []; - return { circles, attempts: 0 }; - }, - - update(app, state, objects) { - // Try to add a new circle each frame - const maxAttempts = 100; - const maxCircles = 150; - - if (objects.circles.length >= maxCircles) return; - - for (let i = 0; i < 10; i++) { - const x = Math.random() * app.screen.width; - const y = Math.random() * app.screen.height; - const minRadius = 5; - const maxRadius = 60; - - let valid = true; - let radius = minRadius; - - // Find largest radius that doesn't overlap - for (let r = minRadius; r < maxRadius; r++) { - let overlaps = false; - - for (const other of objects.circles) { - const dx = x - other.x; - const dy = y - other.y; - const dist = Math.sqrt(dx * dx + dy * dy); - - if (dist < r + other.radius + 2) { - overlaps = true; - break; - } - } - - if (overlaps) { - break; - } - radius = r; - } - - if (radius > minRadius) { - const circle = new PIXI.Graphics(); - circle.circle(0, 0, radius); - const hue = (objects.circles.length * 137.5) % 360; - circle.fill(hslToHex(hue, 70, 60)); - circle.x = x; - circle.y = y; - circle.radius = radius; - - app.stage.addChild(circle); - objects.circles.push(circle); - break; - } - } - }, - - cleanup(app, objects) { - objects.circles.forEach(c => c.destroy()); - } -}; - -// ============================================================================ -// DEMO 6: PERLIN FLOW FIELD -// ============================================================================ - -export const flowField = { - name: "Flow Field", - description: "Particles following a noise field", - - setup(app, state) { - const particles = []; - const colors = [0xff6b6b, 0x4ecdc4, 0x45b7d1, 0xf9ca24, 0x6c5ce7, 0xfeca57]; - - for (let i = 0; i < 300; i++) { - const particle = new PIXI.Graphics(); - particle.circle(0, 0, 2); - particle.fill(colors[Math.floor(Math.random() * colors.length)]); - particle.alpha = 0.6; - - particle.x = Math.random() * app.screen.width; - particle.y = Math.random() * app.screen.height; - particle.vx = 0; - particle.vy = 0; - particle.color = colors[Math.floor(Math.random() * colors.length)]; - - app.stage.addChild(particle); - particles.push(particle); - } - - return { particles }; - }, - - update(app, state, objects) { - const t = state.state.time.current; - - objects.particles.forEach(p => { - // Simple noise-like function using sin/cos - const angle = noise(p.x * 0.005, p.y * 0.005, t * 0.3) * Math.PI * 2; - - p.vx += Math.cos(angle) * 0.3; - p.vy += Math.sin(angle) * 0.3; - - // Damping - p.vx *= 0.95; - p.vy *= 0.95; - - p.x += p.vx; - p.y += p.vy; - - // Wrap around screen - if (p.x < 0) p.x = app.screen.width; - if (p.x > app.screen.width) p.x = 0; - if (p.y < 0) p.y = app.screen.height; - if (p.y > app.screen.height) p.y = 0; - }); - }, - - cleanup(app, objects) { - objects.particles.forEach(p => p.destroy()); - } -}; - -// ============================================================================ -// DEMO 7: DNA HELIX -// ============================================================================ - -export const dnaHelix = { - name: "DNA Helix", - description: "Rotating double helix structure", - - setup(app, state) { - const helix1 = []; - const helix2 = []; - const connectors = []; - const segments = 40; - - for (let i = 0; i < segments; i++) { - const sphere1 = new PIXI.Graphics(); - sphere1.circle(0, 0, 8); - sphere1.fill(0x4ecdc4); - app.stage.addChild(sphere1); - helix1.push(sphere1); - - const sphere2 = new PIXI.Graphics(); - sphere2.circle(0, 0, 8); - sphere2.fill(0xff6b6b); - app.stage.addChild(sphere2); - helix2.push(sphere2); - - const connector = new PIXI.Graphics(); - app.stage.addChild(connector); - connectors.push(connector); - } - - return { helix1, helix2, connectors }; - }, - - update(app, state, objects) { - const t = state.state.time.current; - const cx = app.screen.width / 2; - const cy = app.screen.height / 2; - const radius = 100; - const height = app.screen.height * 0.8; - const spacing = height / objects.helix1.length; - - objects.helix1.forEach((sphere, i) => { - const y = i * spacing - height / 2 + cy; - const angle = t + i * 0.3; - const x = cx + Math.cos(angle) * radius; - const z = Math.sin(angle) * radius; - - sphere.x = x; - sphere.y = y; - sphere.scale.set(1 + z / 200); - sphere.alpha = 0.5 + z / 400; - }); - - objects.helix2.forEach((sphere, i) => { - const y = i * spacing - height / 2 + cy; - const angle = t + i * 0.3 + Math.PI; - const x = cx + Math.cos(angle) * radius; - const z = Math.sin(angle) * radius; - - sphere.x = x; - sphere.y = y; - sphere.scale.set(1 + z / 200); - sphere.alpha = 0.5 + z / 400; - }); - - // Draw connectors - objects.connectors.forEach((connector, i) => { - connector.clear(); - connector.moveTo(objects.helix1[i].x, objects.helix1[i].y); - connector.lineTo(objects.helix2[i].x, objects.helix2[i].y); - connector.stroke({ width: 2, color: 0x666666, alpha: 0.3 }); - }); - }, - - cleanup(app, objects) { - objects.helix1.forEach(s => s.destroy()); - objects.helix2.forEach(s => s.destroy()); - objects.connectors.forEach(c => c.destroy()); - } -}; - -// ============================================================================ -// DEMO 8: FIREWORKS -// ============================================================================ - -export const fireworks = { - name: "Fireworks", - description: "Explosive particle celebration", - - setup(app, state) { - return { - explosions: [], - nextExplosion: 0 - }; - }, - - update(app, state, objects) { - const t = state.state.time.current; - - // Create new explosion every second - if (t > objects.nextExplosion) { - objects.nextExplosion = t + 0.5 + Math.random(); - - const explosion = { - x: Math.random() * app.screen.width, - y: Math.random() * app.screen.height * 0.7, - particles: [], - color: Math.random() * 0xffffff, - born: t - }; - - // Create particles - for (let i = 0; i < 50; i++) { - const angle = (i / 50) * Math.PI * 2; - const speed = 2 + Math.random() * 4; - const particle = new PIXI.Graphics(); - particle.circle(0, 0, 3); - particle.fill(explosion.color); - particle.x = explosion.x; - particle.y = explosion.y; - particle.vx = Math.cos(angle) * speed; - particle.vy = Math.sin(angle) * speed; - - app.stage.addChild(particle); - explosion.particles.push(particle); - } - - objects.explosions.push(explosion); - } - - // Update explosions - objects.explosions = objects.explosions.filter(explosion => { - const age = t - explosion.born; - - if (age > 3) { - explosion.particles.forEach(p => p.destroy()); - return false; - } - - explosion.particles.forEach(p => { - p.vx *= 0.98; - p.vy += 0.1; // Gravity - p.x += p.vx; - p.y += p.vy; - p.alpha = 1 - age / 3; - }); - - return true; - }); - }, - - cleanup(app, objects) { - objects.explosions.forEach(explosion => { - explosion.particles.forEach(p => p.destroy()); - }); - } -}; - -// ============================================================================ -// DEMO 9: MATRIX RAIN -// ============================================================================ - -export const matrixRain = { - name: "Matrix Rain", - description: "Falling digital rain effect", - - setup(app, state) { - const fontSize = 16; - const columns = Math.floor(app.screen.width / fontSize); - const drops = []; - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*"; - - for (let i = 0; i < columns; i++) { - const text = new PIXI.Text('', { - fontFamily: 'monospace', - fontSize: fontSize, - fill: 0x00ff00 - }); - text.x = i * fontSize; - text.y = -Math.random() * app.screen.height; - - app.stage.addChild(text); - drops.push({ - text, - speed: 1 + Math.random() * 3, - chars: chars - }); - } - - return { drops }; - }, - - update(app, state, objects) { - objects.drops.forEach(drop => { - drop.y = (drop.y || drop.text.y) + drop.speed; - drop.text.y = drop.y; - - // Random character - if (Math.random() > 0.95) { - drop.text.text = drop.chars[Math.floor(Math.random() * drop.chars.length)]; - } - - // Reset to top - if (drop.y > app.screen.height) { - drop.y = -20; - drop.text.alpha = 1; - } - - // Fade trail - drop.text.alpha = Math.max(0.1, drop.text.alpha - 0.01); - }); - }, - - cleanup(app, objects) { - objects.drops.forEach(d => d.text.destroy()); - } -}; - -// ============================================================================ -// DEMO 10: SOLAR SYSTEM -// ============================================================================ - -export const solarSystem = { - name: "Solar System", - description: "Orbiting planets around a star", - - setup(app, state) { - const cx = app.screen.width / 2; - const cy = app.screen.height / 2; - - // Sun - const sun = new PIXI.Graphics(); - sun.circle(0, 0, 30); - sun.fill(0xffd700); - sun.x = cx; - sun.y = cy; - app.stage.addChild(sun); - - // Planets - const planets = [ - { radius: 60, size: 6, speed: 2.0, color: 0x8b7355 }, - { radius: 100, size: 10, speed: 1.5, color: 0xff6347 }, - { radius: 150, size: 12, speed: 1.0, color: 0x4169e1 }, - { radius: 200, size: 8, speed: 0.7, color: 0xff4500 }, - { radius: 260, size: 18, speed: 0.4, color: 0xdaa520 }, - ]; - - const planetObjects = planets.map(config => { - const planet = new PIXI.Graphics(); - planet.circle(0, 0, config.size); - planet.fill(config.color); - planet.config = config; - app.stage.addChild(planet); - return planet; - }); - - return { sun, planets: planetObjects, cx, cy }; - }, - - update(app, state, objects) { - const t = state.state.time.current; - - objects.planets.forEach((planet, i) => { - const angle = t * planet.config.speed; - planet.x = objects.cx + Math.cos(angle) * planet.config.radius; - planet.y = objects.cy + Math.sin(angle) * planet.config.radius; - }); - }, - - cleanup(app, objects) { - objects.sun.destroy(); - objects.planets.forEach(p => p.destroy()); - } -}; - -// ============================================================================ -// UTILITIES -// ============================================================================ - -function hslToHex(h, s, l) { - s /= 100; - l /= 100; - const c = (1 - Math.abs(2 * l - 1)) * s; - const x = c * (1 - Math.abs((h / 60) % 2 - 1)); - const m = l - c/2; - let r = 0, g = 0, b = 0; - - if (0 <= h && h < 60) { - r = c; g = x; b = 0; - } else if (60 <= h && h < 120) { - r = x; g = c; b = 0; - } else if (120 <= h && h < 180) { - r = 0; g = c; b = x; - } else if (180 <= h && h < 240) { - r = 0; g = x; b = c; - } else if (240 <= h && h < 300) { - r = x; g = 0; b = c; - } else if (300 <= h && h < 360) { - r = c; g = 0; b = x; - } - - r = Math.round((r + m) * 255); - g = Math.round((g + m) * 255); - b = Math.round((b + m) * 255); - - return (r << 16) | (g << 8) | b; -} - -function noise(x, y, z) { - return Math.sin(x + Math.cos(y)) * Math.cos(y + Math.sin(z)) * Math.sin(z + Math.cos(x)); -} - -// ============================================================================ -// EXPORT ALL DEMOS -// ============================================================================ - -export const allDemos = [ - bouncingParticles, - spirograph, - starfield, - waveInterference, - circlePacking, - flowField, - dnaHelix, - fireworks, - matrixRain, - solarSystem -]; diff --git a/web-timeplot/src/example-usage.js b/web-timeplot/src/example-usage.js deleted file mode 100644 index 67eff4b..0000000 --- a/web-timeplot/src/example-usage.js +++ /dev/null @@ -1,535 +0,0 @@ -/** - * Example Usage: Complete examples of the new architecture - * - * This file demonstrates how to use the separated data/visualization architecture: - * - TimeSeriesPlot: Pure visualization - * - DataSource: Data generation/provision - * - Connections: Links between them - */ - -import { Application } from 'pixi.js'; -import { TimeSeriesPlot } from './timeseries-plot.js'; -import { - SyntheticDataSource, - FunctionDataSource, - StreamingDataSource, - WebSocketDataSource, -} from './data-sources.js'; -import { - DirectConnection, - BufferedConnection, - ConnectionManager, - connectSyntheticData, - connectFunction, - createConnectedPlot, -} from './plot-connections.js'; -import { - TestDataFactory, - SineWaveGenerator, - PerlinNoiseGenerator, - ChirpGenerator, -} from './test-data-generators.js'; - -// ============================================================================ -// Example 1: Simple Setup - One plot, one data source -// ============================================================================ - -export async function example1_SimpleSetup() { - console.log('=== Example 1: Simple Setup ==='); - - // Create PixiJS app - const app = new Application(); - await app.init({ - width: 800, - height: 600, - backgroundColor: 0x1a1a26, - }); - document.body.appendChild(app.canvas); - - // Create plot (visualization only) - const plot = new TimeSeriesPlot({ - x: 0, - y: 0, - width: 800, - height: 600, - title: 'Simple Sine Wave', - showGrid: true, - }); - app.stage.addChild(plot.container); - - // Create data source - const generator = TestDataFactory.createSimpleSine(30); - const source = new SyntheticDataSource({ - generator: generator, - pointsPerLine: 100, - width: 800, - lineInterval: 100, // New line every 100ms - }); - - // Connect source to plot - const connection = new DirectConnection(source, plot); - connection.connect(); - - // Update plot every frame - app.ticker.add(() => { - plot.update(); - }); - - return { app, plot, source, connection }; -} - -// ============================================================================ -// Example 2: Quick Setup Using Helper Functions -// ============================================================================ - -export async function example2_QuickSetup() { - console.log('=== Example 2: Quick Setup ==='); - - const app = new Application(); - await app.init({ - width: 800, - height: 600, - backgroundColor: 0x1a1a26, - }); - document.body.appendChild(app.canvas); - - // One-liner setup! - const { plot, source, connection } = createConnectedPlot( - app, - { - x: 0, - y: 0, - width: 800, - height: 600, - title: 'Quick Setup', - }, - { - generator: TestDataFactory.createComplexPattern(30), - lineInterval: 100, - } - ); - - app.ticker.add(() => plot.update()); - - return { app, plot, source, connection }; -} - -// ============================================================================ -// Example 3: Multiple Plots with Different Data Sources -// ============================================================================ - -export async function example3_MultiplePlots() { - console.log('=== Example 3: Multiple Plots ==='); - - const app = new Application(); - await app.init({ - width: 1600, - height: 600, - backgroundColor: 0x1a1a26, - }); - document.body.appendChild(app.canvas); - - const width = 800; - const height = 600; - - // Left plot: Sine wave - const plot1 = new TimeSeriesPlot({ - x: 0, - y: 0, - width: width, - height: height, - title: 'Sine Wave', - color: 0xff6666, - }); - - // Right plot: Perlin noise - const plot2 = new TimeSeriesPlot({ - x: width, - y: 0, - width: width, - height: height, - title: 'Perlin Noise', - color: 0x66ff66, - }); - - app.stage.addChild(plot1.container); - app.stage.addChild(plot2.container); - - // Connect different data sources - const conn1 = connectSyntheticData( - TestDataFactory.createSimpleSine(30), - plot1, - { lineInterval: 100 } - ); - - const conn2 = connectSyntheticData( - TestDataFactory.createSmoothNoise(30), - plot2, - { lineInterval: 100 } - ); - - app.ticker.add(() => { - plot1.update(); - plot2.update(); - }); - - return { app, plots: [plot1, plot2], connections: [conn1, conn2] }; -} - -// ============================================================================ -// Example 4: Using Function-Based Data Source -// ============================================================================ - -export async function example4_FunctionSource() { - console.log('=== Example 4: Function Source ==='); - - const app = new Application(); - await app.init({ width: 800, height: 600, backgroundColor: 0x1a1a26 }); - document.body.appendChild(app.canvas); - - const plot = new TimeSeriesPlot({ - x: 0, - y: 0, - width: 800, - height: 600, - title: 'Custom Function', - }); - app.stage.addChild(plot.container); - - // Define a custom function: (x, t) => y - // x is normalized 0-1 across the width - // t is time in seconds - const customFunc = (x, t) => { - // Create an interference pattern - const wave1 = Math.sin(x * 10 + t * 2); - const wave2 = Math.sin(x * 15 - t * 3); - const wave3 = Math.cos(x * 8 + t * 1.5); - return (wave1 + wave2 + wave3) / 3; - }; - - const connection = connectFunction(customFunc, plot, { - lineInterval: 100, - amplitude: 30, - }); - - app.ticker.add(() => plot.update()); - - return { app, plot, connection }; -} - -// ============================================================================ -// Example 5: Swapping Data Sources at Runtime -// ============================================================================ - -export async function example5_SwappingSources() { - console.log('=== Example 5: Swapping Sources ==='); - - const app = new Application(); - await app.init({ width: 800, height: 600, backgroundColor: 0x1a1a26 }); - document.body.appendChild(app.canvas); - - const plot = new TimeSeriesPlot({ - x: 0, - y: 0, - width: 800, - height: 600, - title: 'Dynamic Source Switching', - }); - app.stage.addChild(plot.container); - - // Start with sine wave - let currentConnection = connectSyntheticData( - TestDataFactory.createSimpleSine(30), - plot, - { lineInterval: 100 } - ); - - app.ticker.add(() => plot.update()); - - // Function to switch to a different data source - const switchToSource = (generator, title) => { - // Disconnect current source - currentConnection.disconnect(); - - // Connect new source - currentConnection = connectSyntheticData(generator, plot, { - lineInterval: 100, - }); - - plot.setTitle(title); - console.log(`Switched to: ${title}`); - }; - - // Example: Switch sources every 5 seconds - let sourceIndex = 0; - const sources = [ - { gen: TestDataFactory.createSimpleSine(30), title: 'Sine Wave' }, - { gen: TestDataFactory.createComplexPattern(30), title: 'Complex Pattern' }, - { gen: TestDataFactory.createSmoothNoise(30), title: 'Perlin Noise' }, - { gen: TestDataFactory.createFrequencySweep(30), title: 'Frequency Sweep' }, - ]; - - setInterval(() => { - sourceIndex = (sourceIndex + 1) % sources.length; - const source = sources[sourceIndex]; - switchToSource(source.gen, source.title); - }, 5000); - - return { app, plot, switchToSource }; -} - -// ============================================================================ -// Example 6: Streaming Data with Buffering -// ============================================================================ - -export async function example6_StreamingData() { - console.log('=== Example 6: Streaming Data ==='); - - const app = new Application(); - await app.init({ width: 800, height: 600, backgroundColor: 0x1a1a26 }); - document.body.appendChild(app.canvas); - - const plot = new TimeSeriesPlot({ - x: 0, - y: 0, - width: 800, - height: 600, - title: 'Streaming Data (Buffered)', - }); - app.stage.addChild(plot.container); - - // Create streaming source (emits individual points) - const generator = new SineWaveGenerator({ - frequency: 2.0, - amplitude: 1.0, - sampleRate: 60, - }); - - const source = new StreamingDataSource({ - generator: generator, - sampleRate: 60, // 60 points per second - }); - - // Use buffered connection to assemble points into lines - const connection = new BufferedConnection(source, plot, { - bufferSize: 100, // Buffer 100 points before creating a line - bufferTimeout: 1000, // Or timeout after 1 second - }); - connection.connect(); - - app.ticker.add(() => plot.update()); - - return { app, plot, source, connection }; -} - -// ============================================================================ -// Example 7: Connection Manager (Managing Multiple Connections) -// ============================================================================ - -export async function example7_ConnectionManager() { - console.log('=== Example 7: Connection Manager ==='); - - const app = new Application(); - await app.init({ width: 800, height: 600, backgroundColor: 0x1a1a26 }); - document.body.appendChild(app.canvas); - - const plot = new TimeSeriesPlot({ - x: 0, - y: 0, - width: 800, - height: 600, - title: 'Managed Connections', - }); - app.stage.addChild(plot.container); - - // Create connection manager - const manager = new ConnectionManager(); - - // Add first connection - const source1 = new SyntheticDataSource({ - generator: TestDataFactory.createSimpleSine(30), - pointsPerLine: 100, - width: 800, - lineInterval: 100, - }); - - const connId1 = manager.connect(source1, plot, { type: 'direct' }); - console.log('Connection ID:', connId1); - - app.ticker.add(() => plot.update()); - - // Later: disconnect and switch to different source - setTimeout(() => { - manager.disconnect(connId1); - - const source2 = new SyntheticDataSource({ - generator: TestDataFactory.createFrequencySweep(30), - pointsPerLine: 100, - width: 800, - lineInterval: 100, - }); - - const connId2 = manager.connect(source2, plot, { type: 'direct' }); - plot.setTitle('Frequency Sweep'); - console.log('Switched to connection:', connId2); - }, 5000); - - return { app, plot, manager }; -} - -// ============================================================================ -// Example 8: Complete Interactive Demo -// ============================================================================ - -export async function example8_InteractiveDemo() { - console.log('=== Example 8: Interactive Demo ==='); - - const app = new Application(); - await app.init({ - width: 1600, - height: 800, - backgroundColor: 0x1a1a26, - }); - document.body.appendChild(app.canvas); - - // Create two plots - const plot1 = new TimeSeriesPlot({ - x: 0, - y: 0, - width: 800, - height: 800, - title: 'Plot 1 - Press 1-5 to change', - color: 0xff6666, - }); - - const plot2 = new TimeSeriesPlot({ - x: 800, - y: 0, - width: 800, - height: 800, - title: 'Plot 2 - Press 6-0 to change', - color: 0x66ff66, - }); - - app.stage.addChild(plot1.container); - app.stage.addChild(plot2.container); - - // Connection manager - const manager = new ConnectionManager(); - - // Available data sources - const dataSources = { - sine: () => TestDataFactory.createSimpleSine(30), - complex: () => TestDataFactory.createComplexPattern(30), - noise: () => TestDataFactory.createSmoothNoise(30), - sweep: () => TestDataFactory.createFrequencySweep(30), - burst: () => TestDataFactory.createBurstySignal(30), - }; - - // Track current connections - let conn1Id = null; - let conn2Id = null; - - // Helper to switch source - const switchSource = (plot, generatorFunc, title) => { - // Disconnect old connection - const connId = plot === plot1 ? conn1Id : conn2Id; - if (connId !== null) { - manager.disconnect(connId); - } - - // Create new connection - const source = new SyntheticDataSource({ - generator: generatorFunc(), - pointsPerLine: 100, - width: plot.width, - lineInterval: 100, - }); - - const newConnId = manager.connect(source, plot, { type: 'direct' }); - plot.setTitle(title); - - // Store connection ID - if (plot === plot1) { - conn1Id = newConnId; - } else { - conn2Id = newConnId; - } - }; - - // Initialize with default sources - switchSource(plot1, dataSources.sine, 'Plot 1 - Sine Wave'); - switchSource(plot2, dataSources.complex, 'Plot 2 - Complex Pattern'); - - // Keyboard controls - window.addEventListener('keydown', (e) => { - switch (e.key) { - case '1': - switchSource(plot1, dataSources.sine, 'Plot 1 - Sine Wave'); - break; - case '2': - switchSource(plot1, dataSources.complex, 'Plot 1 - Complex Pattern'); - break; - case '3': - switchSource(plot1, dataSources.noise, 'Plot 1 - Perlin Noise'); - break; - case '4': - switchSource(plot1, dataSources.sweep, 'Plot 1 - Frequency Sweep'); - break; - case '5': - switchSource(plot1, dataSources.burst, 'Plot 1 - Burst Signal'); - break; - case '6': - switchSource(plot2, dataSources.sine, 'Plot 2 - Sine Wave'); - break; - case '7': - switchSource(plot2, dataSources.complex, 'Plot 2 - Complex Pattern'); - break; - case '8': - switchSource(plot2, dataSources.noise, 'Plot 2 - Perlin Noise'); - break; - case '9': - switchSource(plot2, dataSources.sweep, 'Plot 2 - Frequency Sweep'); - break; - case '0': - switchSource(plot2, dataSources.burst, 'Plot 2 - Burst Signal'); - break; - case 'g': - plot1.setGridVisible(!plot1.showGrid); - plot2.setGridVisible(!plot2.showGrid); - break; - case 'c': - plot1.clearData(); - plot2.clearData(); - break; - } - }); - - // Update loop - app.ticker.add(() => { - plot1.update(); - plot2.update(); - }); - - console.log('Controls:'); - console.log(' 1-5: Change Plot 1 source'); - console.log(' 6-0: Change Plot 2 source'); - console.log(' G: Toggle grid'); - console.log(' C: Clear data'); - - return { app, plot1, plot2, manager }; -} - -// ============================================================================ -// Quick Test: Run one of the examples -// ============================================================================ - -// Uncomment to run an example: -// example1_SimpleSetup(); -// example2_QuickSetup(); -// example3_MultiplePlots(); -// example4_FunctionSource(); -// example5_SwappingSources(); -// example6_StreamingData(); -// example7_ConnectionManager(); -//example8_InteractiveDemo(); diff --git a/web-timeplot/src/main.js b/web-timeplot/src/main.js deleted file mode 100644 index d2b348e..0000000 --- a/web-timeplot/src/main.js +++ /dev/null @@ -1 +0,0 @@ -import './bootstrap.js'; diff --git a/web-timeplot/src/metrics.js b/web-timeplot/src/metrics.js deleted file mode 100644 index fdda10a..0000000 --- a/web-timeplot/src/metrics.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * RollingAverage - Maintains a rolling window of values for smooth averaging - */ -class RollingAverage { - constructor(capacity) { - this.values = []; - this.capacity = capacity; - this.sum = 0; - } - - push(value) { - if (this.values.length >= this.capacity) { - const old = this.values.shift(); - this.sum -= old; - } - this.values.push(value); - this.sum += value; - } - - average() { - return this.values.length > 0 ? this.sum / this.values.length : 0; - } - - min() { - return this.values.length > 0 ? Math.min(...this.values) : 0; - } - - max() { - return this.values.length > 0 ? Math.max(...this.values) : 0; - } -} - -/** - * PerformanceMetrics - Tracks and analyzes frame performance - */ -export class PerformanceMetrics { - constructor(rollingWindow = 60, historyCapacity = 10000) { - // Rolling averages - this.frameTime = new RollingAverage(rollingWindow); - this.updateTime = new RollingAverage(rollingWindow); - this.renderTime = new RollingAverage(rollingWindow); - this.vertexCount = new RollingAverage(rollingWindow); - this.lineCount = new RollingAverage(rollingWindow); - - // History for export - this.history = []; - this.historyCapacity = historyCapacity; - - // Frame timing - this.frameStart = 0; - this.updateStart = 0; - this.renderStart = 0; - - this.totalFrames = 0; - } - - beginFrame() { - this.frameStart = performance.now(); - } - - beginUpdate() { - this.updateStart = performance.now(); - } - - endUpdate() { - const duration = performance.now() - this.updateStart; - return duration; - } - - beginRender() { - this.renderStart = performance.now(); - } - - endRender() { - const duration = performance.now() - this.renderStart; - return duration; - } - - endFrame(updateMs, renderMs, vertexCount, lineCount) { - const totalMs = performance.now() - this.frameStart; - - // Update rolling averages - this.frameTime.push(totalMs); - this.updateTime.push(updateMs); - this.renderTime.push(renderMs); - this.vertexCount.push(vertexCount); - this.lineCount.push(lineCount); - - // Store in history - const record = { - frame: this.totalFrames, - totalMs, - updateMs, - renderMs, - vertexCount, - lineCount, - fps: totalMs > 0 ? 1000 / totalMs : 0, - }; - - if (this.history.length >= this.historyCapacity) { - this.history.shift(); - } - this.history.push(record); - - this.totalFrames++; - } - - getFPS() { - const avg = this.frameTime.average(); - return avg > 0 ? 1000 / avg : 0; - } - - getMinFPS() { - const max = this.frameTime.max(); - return max > 0 ? 1000 / max : 0; - } - - getMaxFPS() { - const min = this.frameTime.min(); - return min > 0 ? 1000 / min : 0; - } - - formatSummary() { - return `FPS: ${this.getFPS().toFixed(1)} (min: ${this.getMinFPS().toFixed(1)}, max: ${this.getMaxFPS().toFixed(1)}) | ` + - `Frame: ${this.frameTime.average().toFixed(2)}ms | ` + - `Update: ${this.updateTime.average().toFixed(2)}ms | ` + - `Render: ${this.renderTime.average().toFixed(2)}ms | ` + - `Vertices: ${Math.round(this.vertexCount.average())} | ` + - `Lines: ${Math.round(this.lineCount.average())}`; - } - - exportToCSV() { - let csv = 'frame,total_ms,update_ms,render_ms,vertex_count,line_count,fps\n'; - - for (const record of this.history) { - csv += `${record.frame},${record.totalMs},${record.updateMs},${record.renderMs},` + - `${record.vertexCount},${record.lineCount},${record.fps}\n`; - } - - return csv; - } -} diff --git a/web-timeplot/src/plot-connections.js b/web-timeplot/src/plot-connections.js deleted file mode 100644 index 0e96dd8..0000000 --- a/web-timeplot/src/plot-connections.js +++ /dev/null @@ -1,392 +0,0 @@ -/** - * Plot Connections - Links data sources to visualization plots - * - * This module manages the connection between data sources and plots, - * handling buffering, timing, and data flow. - * - * Connection Types: - * - DirectConnection: Lines from source → plot (no buffering) - * - BufferedConnection: Points → buffer → lines → plot - * - SynchronizedConnection: Multiple sources → synchronized output - */ - -/** - * Base connection class - */ -class PlotConnection { - constructor(source, plot, config = {}) { - this.source = source; - this.plot = plot; - this.config = config; - this.isActive = false; - this.subscriptions = []; - } - - /** - * Activate the connection - start data flow - */ - connect() { - if (this.isActive) return; - this.isActive = true; - this.setupSubscriptions(); - this.source.start(); - } - - /** - * Deactivate the connection - stop data flow - */ - disconnect() { - if (!this.isActive) return; - this.isActive = false; - this.cleanup(); - this.source.stop(); - } - - /** - * Setup event subscriptions (override in subclasses) - */ - setupSubscriptions() { - throw new Error('setupSubscriptions() must be implemented by subclass'); - } - - /** - * Cleanup subscriptions - */ - cleanup() { - this.subscriptions.forEach(unsub => unsub()); - this.subscriptions = []; - } -} - -/** - * Direct connection - passes lines directly from source to plot - * Use when source emits complete lines of data - */ -export class DirectConnection extends PlotConnection { - setupSubscriptions() { - const unsubLine = this.source.on('line', (data) => { - this.plot.addLine(data.points, data.metadata); - }); - - const unsubError = this.source.on('error', (data) => { - console.error('[DirectConnection] Source error:', data.error); - }); - - this.subscriptions.push(unsubLine, unsubError); - } -} - -/** - * Buffered connection - buffers individual points into lines - * Use when source emits individual data points that need to be assembled - */ -export class BufferedConnection extends PlotConnection { - constructor(source, plot, config = {}) { - super(source, plot, config); - this.buffer = []; - this.bufferSize = config.bufferSize || 100; - this.bufferTimeout = config.bufferTimeout || 1000; // ms - this.lastFlush = Date.now(); - this.flushHandle = null; - - // Start auto-flush timer - if (config.autoFlush !== false) { - this.startAutoFlush(); - } - } - - setupSubscriptions() { - const unsubPoint = this.source.on('point', (data) => { - this.addToBuffer(data); - }); - - const unsubError = this.source.on('error', (data) => { - console.error('[BufferedConnection] Source error:', data.error); - }); - - this.subscriptions.push(unsubPoint, unsubError); - } - - addToBuffer(data) { - this.buffer.push(data); - - // Flush if buffer is full - if (this.buffer.length >= this.bufferSize) { - this.flush(); - } - } - - flush() { - if (this.buffer.length === 0) return; - - // Convert buffer to line points - const points = this.buffer.map((data, idx) => { - const x = (idx / this.buffer.length) * this.plot.width; - return { x, y: data.value }; - }); - - this.plot.addLine(points, { - timestamp: this.lastFlush, - pointCount: this.buffer.length, - }); - - this.buffer = []; - this.lastFlush = Date.now(); - } - - startAutoFlush() { - this.flushHandle = setInterval(() => { - const timeSinceLastFlush = Date.now() - this.lastFlush; - if (timeSinceLastFlush >= this.bufferTimeout && this.buffer.length > 0) { - this.flush(); - } - }, 100); // Check every 100ms - } - - cleanup() { - super.cleanup(); - if (this.flushHandle) { - clearInterval(this.flushHandle); - this.flushHandle = null; - } - } -} - -/** - * Synchronized connection - synchronizes multiple sources to one plot - * Useful for combining multiple data streams - */ -export class SynchronizedConnection extends PlotConnection { - constructor(sources, plot, config = {}) { - super(null, plot, config); // No single source - this.sources = sources; - this.syncMode = config.syncMode || 'wait-for-all'; // 'wait-for-all', 'first-available' - this.lineBuffers = new Map(); // sourceId => latest line - } - - connect() { - if (this.isActive) return; - this.isActive = true; - - this.sources.forEach((source, idx) => { - const unsubLine = source.on('line', (data) => { - this.handleSourceLine(idx, data); - }); - - const unsubError = source.on('error', (data) => { - console.error(`[SynchronizedConnection] Source ${idx} error:`, data.error); - }); - - this.subscriptions.push(unsubLine, unsubError); - source.start(); - }); - } - - disconnect() { - if (!this.isActive) return; - this.isActive = false; - this.cleanup(); - this.sources.forEach(source => source.stop()); - } - - handleSourceLine(sourceIdx, data) { - this.lineBuffers.set(sourceIdx, data); - - if (this.syncMode === 'wait-for-all') { - // Wait until we have data from all sources - if (this.lineBuffers.size === this.sources.length) { - this.emitSynchronized(); - } - } else if (this.syncMode === 'first-available') { - // Emit immediately - this.plot.addLine(data.points, { - ...data.metadata, - sourceIdx, - }); - } - } - - emitSynchronized() { - // For now, just emit the first source's line - // Could implement more sophisticated merging - const firstLine = this.lineBuffers.get(0); - if (firstLine) { - this.plot.addLine(firstLine.points, firstLine.metadata); - } - this.lineBuffers.clear(); - } -} - -/** - * Connection Manager - manages multiple connections - */ -export class ConnectionManager { - constructor() { - this.connections = new Map(); // connectionId => connection - this.nextId = 0; - } - - /** - * Create and register a connection - * @returns {number} connectionId - */ - connect(source, plot, config = {}) { - const type = config.type || 'direct'; - let connection; - - switch (type) { - case 'direct': - connection = new DirectConnection(source, plot, config); - break; - case 'buffered': - connection = new BufferedConnection(source, plot, config); - break; - case 'synchronized': - connection = new SynchronizedConnection(source, plot, config); - break; - default: - throw new Error(`Unknown connection type: ${type}`); - } - - const id = this.nextId++; - this.connections.set(id, connection); - connection.connect(); - - return id; - } - - /** - * Disconnect and remove a connection - */ - disconnect(connectionId) { - const connection = this.connections.get(connectionId); - if (connection) { - connection.disconnect(); - this.connections.delete(connectionId); - } - } - - /** - * Disconnect all connections - */ - disconnectAll() { - this.connections.forEach(connection => connection.disconnect()); - this.connections.clear(); - } - - /** - * Get statistics about connections - */ - getStats() { - return { - activeConnections: this.connections.size, - connections: Array.from(this.connections.entries()).map(([id, conn]) => ({ - id, - isActive: conn.isActive, - type: conn.constructor.name, - })), - }; - } -} - -/** - * Helper functions for common connection patterns - */ - -/** - * Connect a synthetic data source to a plot - * @param {DataGenerator} generator - Test data generator instance - * @param {TimeSeriesPlot} plot - Plot to display data - * @param {Object} config - Configuration options - * @returns {DirectConnection} The connection instance - */ -export function connectSyntheticData(generator, plot, config = {}) { - const { SyntheticDataSource } = require('./data-sources.js'); - - const source = new SyntheticDataSource({ - generator, - pointsPerLine: config.pointsPerLine || 100, - width: plot.width, - lineInterval: config.lineInterval || 100, - }); - - const connection = new DirectConnection(source, plot, config); - connection.connect(); - - return connection; -} - -/** - * Connect a function-based source to a plot - * @param {Function} func - Function (x, t) => y - * @param {TimeSeriesPlot} plot - Plot to display data - * @param {Object} config - Configuration options - * @returns {DirectConnection} The connection instance - */ -export function connectFunction(func, plot, config = {}) { - const { FunctionDataSource } = require('./data-sources.js'); - - const source = new FunctionDataSource({ - func, - pointsPerLine: config.pointsPerLine || 100, - width: plot.width, - amplitude: config.amplitude || 30, - lineInterval: config.lineInterval || 100, - }); - - const connection = new DirectConnection(source, plot, config); - connection.connect(); - - return connection; -} - -/** - * Connect a streaming source to a plot with buffering - * @param {DataGenerator} generator - Test data generator instance - * @param {TimeSeriesPlot} plot - Plot to display data - * @param {Object} config - Configuration options - * @returns {BufferedConnection} The connection instance - */ -export function connectStreamingData(generator, plot, config = {}) { - const { StreamingDataSource } = require('./data-sources.js'); - - const source = new StreamingDataSource({ - generator, - sampleRate: config.sampleRate || 60, - }); - - const connection = new BufferedConnection(source, plot, { - bufferSize: config.bufferSize || 100, - bufferTimeout: config.bufferTimeout || 1000, - }); - connection.connect(); - - return connection; -} - -/** - * Quick setup: Create a plot with a data source in one call - * @param {Application} app - PixiJS application - * @param {Object} plotConfig - Plot configuration - * @param {Object} sourceConfig - Source configuration - * @returns {Object} {plot, source, connection} - */ -export function createConnectedPlot(app, plotConfig, sourceConfig) { - const { TimeSeriesPlot } = require('./timeseries-plot.js'); - const { SyntheticDataSource } = require('./data-sources.js'); - - const plot = new TimeSeriesPlot(plotConfig); - app.stage.addChild(plot.container); - - const source = new SyntheticDataSource({ - generator: sourceConfig.generator, - pointsPerLine: plotConfig.width / 8, // Default: ~8 pixels per point - width: plotConfig.width, - lineInterval: sourceConfig.lineInterval || 100, - }); - - const connection = new DirectConnection(source, plot); - connection.connect(); - - return { plot, source, connection }; -} diff --git a/web-timeplot/src/plot/plot-buffer.js b/web-timeplot/src/plot/plot-buffer.js deleted file mode 100644 index b13cdd8..0000000 --- a/web-timeplot/src/plot/plot-buffer.js +++ /dev/null @@ -1,22 +0,0 @@ -export class PlotBuffer { - constructor(maxPoints = 1600) { - this.maxPoints = maxPoints; - this.points = []; - } - - addPoint(point) { - this.points.push(point); - if (this.points.length > this.maxPoints) { - this.points.splice(0, this.points.length - this.maxPoints); - } - } - - clear() { - this.points = []; - } - - getVisiblePoints(currentPlotTimeMs, windowDurationMs) { - const minTime = currentPlotTimeMs - windowDurationMs; - return this.points.filter((point) => point.timeMs >= minTime && point.timeMs <= currentPlotTimeMs); - } -} diff --git a/web-timeplot/src/plot/timeplot-view.js b/web-timeplot/src/plot/timeplot-view.js deleted file mode 100644 index ce90a1f..0000000 --- a/web-timeplot/src/plot/timeplot-view.js +++ /dev/null @@ -1,442 +0,0 @@ -import { Application, Container, Graphics, Text } from 'pixi.js'; -import { formatDuration, formatValue, formatWallClock } from '../utils-format.js'; - -function clamp(value, min, max) { - return Math.min(max, Math.max(min, value)); -} - -function roundRect(graphics, x, y, width, height, radius, fill, stroke) { - graphics.roundRect(x, y, width, height, radius); - graphics.fill(fill); - graphics.stroke(stroke); -} - -export class TimeplotView { - constructor({ host, panelId = 'primary', title = 'Vertical plot', subtitle = null, showReadouts = true, lineColor = 0x9fd1ff, pointColor = 0xe7f2ff }) { - this.host = host; - this.panelId = panelId; - this.panelTitle = title; - this.panelSubtitle = subtitle; - this.showReadouts = showReadouts; - this.lineColor = lineColor; - this.pointColor = pointColor; - this.app = new Application(); - this.container = new Container(); - this.background = new Graphics(); - this.grid = new Graphics(); - this.axes = new Graphics(); - this.line = new Graphics(); - this.points = new Graphics(); - this.crosshair = new Graphics(); - this.overlay = new Container(); - this.readoutBackground = new Graphics(); - this.axisLabelLayer = new Container(); - this.titleText = new Text({ - text: 'Plot viewport', - style: { - fill: 0xeef4ff, - fontFamily: 'Inter, sans-serif', - fontSize: 16, - }, - }); - this.subtitleText = new Text({ - text: 'Synthetic data stream', - style: { - fill: 0x8ca3c7, - fontFamily: 'Inter, sans-serif', - fontSize: 12, - }, - }); - this.realTimeText = new Text({ - text: '', - style: { - fill: 0xe8eef7, - fontFamily: 'IBM Plex Mono, monospace', - fontSize: 11, - }, - }); - this.plotTimeText = new Text({ - text: '', - style: { - fill: 0xe8eef7, - fontFamily: 'IBM Plex Mono, monospace', - fontSize: 11, - }, - }); - this.axisTitleText = new Text({ - text: '', - style: { - fill: 0x90a0b7, - fontFamily: 'Inter, sans-serif', - fontSize: 10, - fontWeight: '600', - letterSpacing: 1.5, - }, - }); - this.screenPoints = []; - this.bounds = { width: 100, height: 100 }; - this.hoverRadiusPx = 20; - this.pointer = null; - this.lastPointerEventAt = 0; - this.axisLabels = []; - } - - async init() { - const rendererPreference = navigator.gpu ? 'webgpu' : 'webgl'; - await this.app.init({ - preference: rendererPreference, - resizeTo: this.host, - antialias: true, - backgroundAlpha: 0, - resolution: Math.min(window.devicePixelRatio || 1, 2), - }); - - this.app.stage.addChild(this.container); - this.container.addChild(this.background); - this.container.addChild(this.grid); - this.container.addChild(this.axes); - this.container.addChild(this.line); - this.container.addChild(this.points); - this.container.addChild(this.crosshair); - this.container.addChild(this.overlay); - this.overlay.addChild(this.readoutBackground); - this.overlay.addChild(this.axisLabelLayer); - this.overlay.addChild(this.titleText); - this.overlay.addChild(this.subtitleText); - this.overlay.addChild(this.realTimeText); - this.overlay.addChild(this.plotTimeText); - this.overlay.addChild(this.axisTitleText); - this.host.appendChild(this.app.canvas); - this.attachPointerListeners(); - - return rendererPreference; - } - - attachPointerListeners() { - this.host.addEventListener('pointerleave', () => { - this.pointer = null; - this.lastPointerEventAt = performance.now(); - }); - - this.host.addEventListener('pointermove', (event) => { - const rect = this.host.getBoundingClientRect(); - this.pointer = { - x: event.clientX - rect.left, - y: event.clientY - rect.top, - }; - this.lastPointerEventAt = performance.now(); - }); - } - - resize() { - this.bounds = { - width: this.host.clientWidth, - height: this.host.clientHeight, - }; - } - - render(state, points) { - this.resize(); - this.renderFrame(state, points); - this.clearHover(); - } - - clearHover() { - this.crosshair.clear(); - } - - getHoverCandidate() { - if (!this.pointer || this.screenPoints.length === 0) { - return null; - } - - let nearestPoint = null; - let nearestDistance = Infinity; - - for (const point of this.screenPoints) { - const dx = point.x - this.pointer.x; - const dy = point.y - this.pointer.y; - const distance = Math.sqrt(dx * dx + dy * dy); - if (distance < nearestDistance) { - nearestPoint = point; - nearestDistance = distance; - } - } - - if (!nearestPoint || nearestDistance > this.hoverRadiusPx) { - return null; - } - - return { - panelId: this.panelId, - point: nearestPoint, - x: clamp(nearestPoint.x, 0, this.bounds.width), - y: clamp(nearestPoint.y, 0, this.bounds.height), - pointerX: this.pointer.x, - pointerY: this.pointer.y, - distance: nearestDistance, - lastPointerEventAt: this.lastPointerEventAt, - }; - } - - hasPointer() { - return this.pointer !== null; - } - - findNearestScreenPointByTime(timeMs) { - if (this.screenPoints.length === 0) { - return null; - } - - let nearestPoint = null; - let nearestDelta = Infinity; - - for (const point of this.screenPoints) { - const delta = Math.abs(point.timeMs - timeMs); - if (delta < nearestDelta) { - nearestPoint = point; - nearestDelta = delta; - } - } - - return nearestPoint; - } - - renderLinkedHover(hoverPoint) { - this.crosshair.clear(); - - if (!hoverPoint) { - return; - } - - const x = clamp(hoverPoint.x, 0, this.bounds.width); - const y = clamp(hoverPoint.y, 0, this.bounds.height); - - this.crosshair.moveTo(x, 0); - this.crosshair.lineTo(x, this.bounds.height); - this.crosshair.moveTo(0, y); - this.crosshair.lineTo(this.bounds.width, y); - this.crosshair.stroke({ color: 0x8cb8ff, width: 1, alpha: 0.24 }); - this.crosshair.rect(x - 4, y - 4, 8, 8); - this.crosshair.stroke({ color: 0xffffff, width: 1.5, alpha: 0.95 }); - } - - ensureAxisLabelCount(count) { - while (this.axisLabels.length < count) { - const label = new Text({ - text: '', - style: { - fill: 0x90a0b7, - fontFamily: 'IBM Plex Mono, monospace', - fontSize: 10, - }, - }); - this.axisLabels.push(label); - this.axisLabelLayer.addChild(label); - } - - while (this.axisLabels.length > count) { - const label = this.axisLabels.pop(); - this.axisLabelLayer.removeChild(label); - label.destroy(); - } - } - - renderAxes({ padding, plotWidth, plotHeight, minTime, maxTime, minValue, maxValue, width }) { - const axisColor = 0x3e4c5f; - const tickColor = 0x4f627a; - const timeTickCount = 5; - const valueTickCount = 5; - const labels = []; - - this.axes.clear(); - this.axes.moveTo(padding.left, padding.top); - this.axes.lineTo(padding.left, padding.top + plotHeight); - this.axes.lineTo(padding.left + plotWidth, padding.top + plotHeight); - this.axes.stroke({ color: axisColor, width: 1, alpha: 1 }); - - for (let index = 0; index < timeTickCount; index += 1) { - const ratio = timeTickCount === 1 ? 0 : index / (timeTickCount - 1); - const y = padding.top + ratio * plotHeight; - const timeMs = minTime + ratio * (maxTime - minTime); - - this.axes.moveTo(padding.left - 8, y); - this.axes.lineTo(padding.left, y); - this.axes.stroke({ color: tickColor, width: 1, alpha: 1 }); - - labels.push({ - text: formatDuration(timeMs), - x: 14, - y: y - 7, - anchorX: 0, - }); - } - - for (let index = 0; index < valueTickCount; index += 1) { - const ratio = valueTickCount === 1 ? 0 : index / (valueTickCount - 1); - const x = padding.left + ratio * plotWidth; - const value = minValue + ratio * (maxValue - minValue); - - this.axes.moveTo(x, padding.top + plotHeight); - this.axes.lineTo(x, padding.top + plotHeight + 8); - this.axes.stroke({ color: tickColor, width: 1, alpha: 1 }); - - labels.push({ - text: formatValue(value), - x, - y: padding.top + plotHeight + 10, - anchorX: 0.5, - }); - } - - this.ensureAxisLabelCount(labels.length); - labels.forEach((config, index) => { - const label = this.axisLabels[index]; - label.text = config.text; - label.x = config.x; - label.y = config.y; - label.anchor.set(config.anchorX, 0); - }); - - this.axisTitleText.text = 'TIME'; - this.axisTitleText.x = 18; - this.axisTitleText.y = padding.top - 18; - this.axisTitleText.rotation = 0; - - this.axes.moveTo(padding.left + plotWidth, padding.top + plotHeight); - this.axes.lineTo(width - 14, padding.top + plotHeight); - this.axes.stroke({ color: 0x202a35, width: 1, alpha: 1 }); - } - - renderReadouts(state, width) { - if (!this.showReadouts) { - this.readoutBackground.clear(); - this.realTimeText.text = ''; - this.plotTimeText.text = ''; - return; - } - - const boxWidth = 168; - const boxHeight = 22; - const gap = 6; - const left = width - boxWidth - 18; - const top = 14; - - this.readoutBackground.clear(); - this.readoutBackground.rect(left, top, boxWidth, boxHeight); - this.readoutBackground.fill({ color: 0x10161d, alpha: 1 }); - this.readoutBackground.stroke({ color: 0x2f3c4d, width: 1, alpha: 1 }); - this.readoutBackground.rect(left, top + boxHeight + gap, boxWidth, boxHeight); - this.readoutBackground.fill({ color: 0x10161d, alpha: 1 }); - this.readoutBackground.stroke({ color: 0x2f3c4d, width: 1, alpha: 1 }); - - this.realTimeText.text = `REAL ${formatWallClock(state.time.realNowMs)}`; - this.realTimeText.x = left + 10; - this.realTimeText.y = top + 5; - - this.plotTimeText.text = `PLOT ${formatDuration(state.time.plotTimeMs)}`; - this.plotTimeText.x = left + 10; - this.plotTimeText.y = top + boxHeight + gap + 5; - } - - renderFrame(state, points) { - const width = this.bounds.width; - const height = this.bounds.height; - const padding = { top: 72, right: 28, bottom: 46, left: 88 }; - const plotWidth = Math.max(10, width - padding.left - padding.right); - const plotHeight = Math.max(10, height - padding.top - padding.bottom); - const minTime = state.time.plotTimeMs - state.plot.windowDurationMs; - const maxTime = Math.max(state.time.plotTimeMs, minTime + 1); - const { min: minValue, max: maxValue } = state.plot.valueRange; - const valueSpan = Math.max(0.001, maxValue - minValue); - - this.background.clear(); - roundRect( - this.background, - 0, - 0, - width, - height, - 6, - { color: 0x05070b, alpha: 1 }, - { color: 0x2c3b4d, width: 1 }, - ); - - this.grid.clear(); - if (state.plot.showGrid) { - const gridColor = 0x21344a; - for (let x = 0; x <= 6; x += 1) { - const px = padding.left + (plotWidth * x) / 6; - this.grid.moveTo(px, padding.top); - this.grid.lineTo(px, padding.top + plotHeight); - this.grid.stroke({ color: gridColor, width: 1, alpha: 0.85 }); - } - - for (let y = 0; y <= 8; y += 1) { - const py = padding.top + (plotHeight * y) / 8; - this.grid.moveTo(padding.left, py); - this.grid.lineTo(padding.left + plotWidth, py); - this.grid.stroke({ color: gridColor, width: 1, alpha: 0.85 }); - } - } - - this.renderAxes({ - padding, - plotWidth, - plotHeight, - minTime, - maxTime, - minValue, - maxValue, - width, - }); - - this.line.clear(); - this.points.clear(); - this.screenPoints = []; - - if (points.length > 0) { - points.forEach((point, index) => { - const x = padding.left + ((point.value - minValue) / valueSpan) * plotWidth; - const y = padding.top + ((point.timeMs - minTime) / (maxTime - minTime)) * plotHeight; - - this.screenPoints.push({ ...point, x, y }); - - if (index === 0) { - this.line.moveTo(x, y); - } else { - this.line.lineTo(x, y); - } - }); - - this.line.stroke({ - color: this.lineColor, - width: 2, - alpha: 0.96, - cap: 'square', - join: 'miter', - }); - - if (state.plot.showPoints) { - for (const point of this.screenPoints) { - this.points.rect(point.x - 2, point.y - 2, 4, 4); - this.points.fill({ color: this.pointColor, alpha: 0.92 }); - } - } - } - - this.titleText.text = this.panelTitle; - this.titleText.x = 20; - this.titleText.y = 14; - - this.subtitleText.text = this.panelSubtitle ?? `value → ${state.source.preset} · ${state.source.sampleRateHz} hz · time ↓`; - this.subtitleText.x = 20; - this.subtitleText.y = 36; - - this.renderReadouts(state, width); - } - - destroy() { - this.app.destroy(true, { children: true }); - } -} diff --git a/web-timeplot/src/state.js b/web-timeplot/src/state.js deleted file mode 100644 index 53d8279..0000000 --- a/web-timeplot/src/state.js +++ /dev/null @@ -1,420 +0,0 @@ -/** - * StateManager - Centralized state management with Proxy-based reactivity - * - * Usage: - * state.time.speed = 2.0 // automatically emits events - * state.on('time.speed', (value) => console.log('Speed changed:', value)) - * state.on('time.*', (change) => console.log('Time domain changed:', change)) - * - * State Domains: - * - userPrefs: showGrid, showMetrics, theme, etc. - * - uiConfig: active panels, layout, dimensions - * - time: current time, speed, paused state, real elapsed time - * - rendering: graphs, renderer info - * - health: framerate, service connections, db access - * - dataInput: sources, structure, metadata - * - inputActions: keyboard/mouse/gamepad action mappings - */ - -// Simple EventEmitter implementation -class EventEmitter { - constructor() { - this.events = new Map(); - } - - on(event, callback) { - if (!this.events.has(event)) { - this.events.set(event, []); - } - this.events.get(event).push(callback); - - // Return unsubscribe function - return () => this.off(event, callback); - } - - off(event, callback) { - if (!this.events.has(event)) return; - const callbacks = this.events.get(event); - const index = callbacks.indexOf(callback); - if (index > -1) { - callbacks.splice(index, 1); - } - } - - emit(event, data) { - if (!this.events.has(event)) return; - this.events.get(event).forEach(callback => { - try { - callback(data); - } catch (e) { - console.error(`[State] Error in event handler for '${event}':`, e); - } - }); - } - - once(event, callback) { - const wrapper = (data) => { - callback(data); - this.off(event, wrapper); - }; - this.on(event, wrapper); - } - - clear() { - this.events.clear(); - } -} - -export class StateManager extends EventEmitter { - constructor() { - super(); - - // Internal state storage (not proxied) - this._state = { - userPrefs: { - showGrid: true, - showMetrics: true, - theme: 'dark', - rollingWindow: 60, - historyCapacity: 10000, - metricsUpdateInterval: 10, - }, - - uiConfig: { - activePanels: ['graph1', 'graph2'], - layout: 'horizontal-split', - canvasWidth: 0, - canvasHeight: 0, - }, - - time: { - current: 0, // Current plot time - realElapsed: 0, // Real time elapsed since start - speed: 1.0, // Time speed multiplier (0.1 to 5.0) - isPaused: false, // Pause state - startTimestamp: Date.now(), // Real timestamp when started - verticalScale: 1.0, // Vertical zoom for time history - }, - - rendering: { - rendererType: 'unknown', // 'webgpu' | 'webgl' | 'canvas' - frameCounter: 0, - // Note: graph instances are NOT stored here to avoid proxy wrapping - }, - - health: { - fps: 0, - updateMs: 0, - renderMs: 0, - vertexCount: 0, - lineCount: 0, - serviceConnections: {}, // e.g., { websocket: 'connected', mqtt: 'disconnected' } - }, - - dataInput: { - sources: [], // Array of data source configs - activeSource: null, // Currently active source - dataStructure: null, // Schema of incoming data - metadata: {}, // Additional metadata - }, - - inputActions: { - keyboardMap: new Map(), // Map of KeyboardEvent.code => action name - mouseMap: new Map(), // Map of mouse button => action name - actionHandlers: new Map(), // Map of action name => handler function - }, - }; - - // Track which domains should be persisted - this._persistedDomains = new Set(['userPrefs']); - - // Load persisted state - this._loadPersistedState(); - - // Create proxied state - this is what users interact with - this.state = this._createProxy(this._state, []); - } - - /** - * Create a reactive Proxy that emits events on property changes - * @param {Object} target - The object to proxy - * @param {Array} path - Current property path (e.g., ['time', 'speed']) - * @private - */ - _createProxy(target, path) { - // Don't proxy non-objects or special objects like Map/Set - if (typeof target !== 'object' || target === null) { - return target; - } - - // Don't proxy Maps and Sets - they need special handling - if (target instanceof Map || target instanceof Set) { - return target; - } - - const self = this; - - return new Proxy(target, { - get(obj, prop) { - const value = obj[prop]; - - // Return primitives and functions as-is - if (typeof value !== 'object' || value === null) { - return value; - } - - // Return nested objects as proxies - return self._createProxy(value, [...path, prop]); - }, - - set(obj, prop, value) { - const oldValue = obj[prop]; - - // Only emit if value actually changed - if (oldValue === value) { - return true; - } - - obj[prop] = value; - - // Build event path - const fullPath = [...path, prop]; - const pathString = fullPath.join('.'); - const domain = fullPath[0]; - - // Emit specific property change: "time.speed" - self.emit(pathString, { - path: fullPath, - value: value, - oldValue: oldValue, - }); - - // Emit domain wildcard: "time.*" - if (domain) { - self.emit(`${domain}.*`, { - path: fullPath, - property: prop, - value: value, - oldValue: oldValue, - }); - } - - // Emit global wildcard: "*" - self.emit('*', { - path: fullPath, - value: value, - oldValue: oldValue, - }); - - // Auto-persist certain domains - if (self._persistedDomains.has(domain)) { - self._persistDomain(domain); - } - - return true; - } - }); - } - - // ========================================================================= - // Persistence - // ========================================================================= - - _persistDomain(domain) { - try { - const data = this._state[domain]; - // Convert Maps to objects for JSON serialization - const serializable = this._makeSerializable(data); - localStorage.setItem(`timeplot-${domain}`, JSON.stringify(serializable)); - } catch (e) { - console.warn(`[State] Failed to persist ${domain}:`, e); - } - } - - _loadPersistedState() { - this._persistedDomains.forEach(domain => { - try { - const saved = localStorage.getItem(`timeplot-${domain}`); - if (saved) { - const data = JSON.parse(saved); - // Deep merge to preserve defaults for new properties - this._state[domain] = this._deepMerge(this._state[domain], data); - } - } catch (e) { - console.warn(`[State] Failed to load ${domain}:`, e); - } - }); - } - - _makeSerializable(obj) { - if (obj instanceof Map) { - return Object.fromEntries(obj); - } - if (obj instanceof Set) { - return Array.from(obj); - } - if (typeof obj === 'object' && obj !== null) { - const result = {}; - for (const [key, value] of Object.entries(obj)) { - result[key] = this._makeSerializable(value); - } - return result; - } - return obj; - } - - _deepMerge(target, source) { - const result = { ...target }; - for (const key in source) { - if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) { - result[key] = this._deepMerge(target[key] || {}, source[key]); - } else { - result[key] = source[key]; - } - } - return result; - } - - // ========================================================================= - // Convenience Methods - // ========================================================================= - - /** - * Toggle a boolean preference - */ - togglePref(key) { - const current = this.state.userPrefs[key]; - if (typeof current === 'boolean') { - this.state.userPrefs[key] = !current; - } - } - - /** - * Pause/resume time - */ - togglePause() { - this.state.time.isPaused = !this.state.time.isPaused; - } - - /** - * Set time speed (clamped 0.1 to 5.0) - */ - setTimeSpeed(speed) { - this.state.time.speed = Math.max(0.1, Math.min(5.0, speed)); - } - - /** - * Increment time (respects pause and speed) - */ - incrementTime(delta) { - if (this.state.time.isPaused) return; - this.state.time.current += delta * this.state.time.speed; - } - - /** - * Update real elapsed time - */ - updateRealElapsed() { - const elapsed = (Date.now() - this.state.time.startTimestamp) / 1000; - this.state.time.realElapsed = elapsed; - } - - // ========================================================================= - // Input Actions System - // ========================================================================= - - /** - * Register an input action handler - * @param {string} actionName - Name of the action (e.g., 'toggleGrid', 'pause') - * @param {Function} handler - Handler function to call - */ - registerAction(actionName, handler) { - this.state.inputActions.actionHandlers.set(actionName, handler); - } - - /** - * Map a keyboard key to an action - * @param {string} code - KeyboardEvent.code (e.g., 'KeyG', 'Space') - * @param {string} actionName - Action to trigger - */ - mapKey(code, actionName) { - this.state.inputActions.keyboardMap.set(code, actionName); - } - - /** - * Map a mouse button to an action - * @param {number} button - Mouse button number (0=left, 1=middle, 2=right) - * @param {string} actionName - Action to trigger - */ - mapMouseButton(button, actionName) { - this.state.inputActions.mouseMap.set(button, actionName); - } - - /** - * Execute an action by name - */ - executeAction(actionName, event) { - const handler = this.state.inputActions.actionHandlers.get(actionName); - if (handler) { - handler(event); - } else { - console.warn(`[State] No handler registered for action: ${actionName}`); - } - } - - /** - * Handle keyboard event through action system - */ - handleKeyboardEvent(event) { - const actionName = this.state.inputActions.keyboardMap.get(event.code); - if (actionName) { - this.executeAction(actionName, event); - return true; - } - return false; - } - - /** - * Handle mouse button event through action system - */ - handleMouseButtonEvent(event) { - const actionName = this.state.inputActions.mouseMap.get(event.button); - if (actionName) { - this.executeAction(actionName, event); - return true; - } - return false; - } - - // ========================================================================= - // Data Sources - // ========================================================================= - - addDataSource(source) { - this.state.dataInput.sources.push(source); - } - - removeDataSource(sourceId) { - const sources = this.state.dataInput.sources; - const index = sources.findIndex(s => s.id === sourceId); - if (index > -1) { - sources.splice(index, 1); - } - } - - setActiveDataSource(sourceId) { - this.state.dataInput.activeSource = sourceId; - } - - // ========================================================================= - // Debugging - // ========================================================================= - - dump() { - console.log('[State] Current state:', JSON.parse(JSON.stringify(this._state))); - } - - debugEvents() { - console.log('[State] Registered events:', Array.from(this.events.keys())); - } -} diff --git a/web-timeplot/src/styles.css b/web-timeplot/src/styles.css deleted file mode 100644 index 6b0477f..0000000 --- a/web-timeplot/src/styles.css +++ /dev/null @@ -1,401 +0,0 @@ -:root { - color-scheme: dark; - --bg: #0a0c10; - --bg-alt: #0f1319; - --surface: #11161d; - --surface-strong: #0d1117; - --surface-raised: #171d26; - --border: #28313d; - --border-strong: #394657; - --text: #edf2f7; - --muted: #97a3b4; - --accent: #9fc7ff; - --accent-strong: #d8e8ff; - --shadow: none; - font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -} - -* { - box-sizing: border-box; -} - -html, -body, -#app { - width: 100%; - height: 100%; - margin: 0; -} - -body { - background: - linear-gradient(180deg, #080a0d 0%, #0d1015 100%); - color: var(--text); - overflow: hidden; -} - -button, -input, -select { - font: inherit; -} - -.timeplot-shell { - display: grid; - grid-template-columns: minmax(0, 1fr) 340px; - grid-template-rows: auto minmax(0, 1fr); - width: 100%; - height: 100%; - gap: 10px; - padding: 10px; -} - -.timeplot-topbar { - grid-column: 1 / -1; - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 12px 14px; - border: 1px solid var(--border-strong); - background: var(--surface); - border-radius: 4px; - box-shadow: var(--shadow); -} - -.timeplot-brand { - display: flex; - flex-direction: column; - gap: 2px; -} - -.timeplot-title { - margin: 0; - font-size: 1rem; - letter-spacing: 0.08em; - text-transform: uppercase; - font-weight: 700; -} - -.timeplot-subtitle { - color: var(--muted); - font-size: 0.78rem; - letter-spacing: 0.04em; - text-transform: uppercase; -} - -.timeplot-toolbar { - display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; - justify-content: flex-end; -} - -.control-group { - display: flex; - align-items: center; - gap: 8px; - padding: 6px 8px; - background: var(--surface-raised); - border: 1px solid var(--border); - border-radius: 3px; -} - -.control-group label, -.control-group span { - color: var(--muted); - font-size: 0.74rem; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.control-group input[type='range'] { - width: 118px; -} - -.control-group input[type='range'] { - accent-color: var(--accent); -} - -.control-button, -.panel-toggle { - color: var(--text); - background: var(--surface); - border: 1px solid var(--border-strong); - border-radius: 2px; - padding: 7px 11px; - cursor: pointer; - transition: border-color 120ms ease, background 120ms ease, color 120ms ease; - text-transform: uppercase; - letter-spacing: 0.08em; - font-size: 0.72rem; - line-height: 1; -} - -.control-button:hover, -.panel-toggle:hover { - border-color: var(--accent); - color: var(--accent-strong); -} - -.control-button[data-active='true'], -.panel-toggle[data-active='true'] { - background: #1a2230; - border-color: var(--accent); - color: var(--accent-strong); -} - -.timeplot-viewport { - position: relative; - min-height: 0; - border-radius: 4px; - overflow: hidden; - border: 1px solid var(--border-strong); - background: #06080b; - box-shadow: var(--shadow); - padding: 10px; -} - -.timeplot-plot-grid { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: 10px; - width: 100%; - height: 100%; - min-height: 0; -} - -.timeplot-plot-panel { - position: relative; - min-width: 0; - min-height: 0; - border: 1px solid var(--border); - background: #070a0d; -} - -.timeplot-canvas-host { - width: 100%; - height: 100%; -} - -.timeplot-sidebar { - display: flex; - flex-direction: column; - gap: 10px; - min-height: 0; - overflow-y: auto; - padding-right: 2px; -} - -.panel { - border: 1px solid var(--border-strong); - background: var(--surface-strong); - border-radius: 4px; - padding: 14px; -} - -.panel[hidden] { - display: none; -} - -.panel h2 { - margin: 0 0 12px; - font-size: 0.8rem; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.panel-subsection + .panel-subsection { - margin-top: 14px; - padding-top: 14px; - border-top: 1px solid var(--border); -} - -.panel-section-title { - margin-bottom: 10px; - color: var(--accent-strong); - font-size: 0.72rem; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.kv-list { - display: grid; - grid-template-columns: auto 1fr; - gap: 10px 12px; - align-items: center; - margin: 0; -} - -.kv-list dt { - color: var(--muted); - font-size: 0.73rem; - letter-spacing: 0.05em; - text-transform: uppercase; -} - -.kv-list dd { - margin: 0; - text-align: right; - font-variant-numeric: tabular-nums; - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; -} - -.field-grid { - display: grid; - gap: 12px; -} - -.field-grid label { - display: grid; - gap: 6px; - color: var(--muted); - font-size: 0.74rem; - letter-spacing: 0.05em; - text-transform: uppercase; -} - -.field-grid[data-source-mode][hidden] { - display: none; -} - -.source-meta { - min-height: 20px; - color: var(--muted); - font-size: 0.76rem; - line-height: 1.4; -} - -.source-meta-error { - color: #ff9d9d; -} - -.source-meta-status { - text-transform: uppercase; - letter-spacing: 0.06em; -} - -.source-meta-status-connected { - color: #99e2b4; -} - -.source-meta-status-connecting { - color: #ffd27f; -} - -.source-meta-status-disconnected, -.source-meta-status-idle { - color: var(--muted); -} - -.source-meta-status-error { - color: #ff9d9d; -} - -.field-grid input, -.field-grid select { - width: 100%; - padding: 9px 10px; - border-radius: 2px; - border: 1px solid var(--border); - background: var(--surface-raised); - color: var(--text); -} - -.field-grid input:focus, -.field-grid select:focus { - outline: none; - border-color: var(--accent); -} - -.panel-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - color: var(--muted); - font-size: 0.74rem; - letter-spacing: 0.05em; - text-transform: uppercase; -} - -.panel-row + .panel-row { - margin-top: 10px; -} - -.panel-row input[type='checkbox'] { - inline-size: 16px; - block-size: 16px; - accent-color: var(--accent); -} - -.muted { - color: var(--muted); -} - -.help-list { - display: grid; - gap: 8px; - margin: 0; - padding-left: 18px; - color: var(--muted); - font-size: 0.82rem; -} - -.timeplot-tooltip { - position: absolute; - min-width: 180px; - padding: 10px 12px; - border-radius: 3px; - border: 1px solid var(--border-strong); - background: #0d1218; - box-shadow: var(--shadow); - pointer-events: none; - transform: translate(12px, -50%); - z-index: 10; -} - -.timeplot-tooltip[hidden] { - display: none; -} - -.timeplot-tooltip-title { - margin-bottom: 8px; - font-size: 0.72rem; - color: var(--accent-strong); - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.timeplot-tooltip-row { - display: flex; - justify-content: space-between; - gap: 16px; - font-size: 0.78rem; -} - -.timeplot-tooltip-row + .timeplot-tooltip-row { - margin-top: 4px; -} - -.timeplot-empty { - color: var(--muted); - font-size: 0.85rem; -} - -@media (max-width: 1100px) { - .timeplot-shell { - grid-template-columns: minmax(0, 1fr); - grid-template-rows: auto minmax(360px, 1fr) auto; - } - - .timeplot-plot-grid { - grid-template-columns: minmax(0, 1fr); - grid-template-rows: repeat(2, minmax(260px, 1fr)); - } - - .timeplot-sidebar { - overflow: visible; - } -} diff --git a/web-timeplot/src/template-for-standard-site.js b/web-timeplot/src/template-for-standard-site.js deleted file mode 100644 index 54aacc7..0000000 --- a/web-timeplot/src/template-for-standard-site.js +++ /dev/null @@ -1,75 +0,0 @@ -//import { setupRenderSystem } from './render.js'; - -let ENVURL = "" //remote server from which to grab env -let env = {}; -let cfg = {}; //the user config -let dom = { - input: {}, - label: {}, - box: {}, //an info-containing box - icon: {}, - info: {} -}; - - -//APP START HERE -$(document).ready(async function() { - console.log('asdf'); - //the core loop of the client application - // 1. setup relationship with DOM and grab references to its elements - log('init DOM'); - await initDOM(); - - log('init cfg'); - await initCfg(); - - log('get env vars'); - await getServerEnvVars(); - - log('init services'); - await initServices(); - - //setupRenderSystem(); - - -}); - -//gets user config from local storage if there is any -function initCfg(){ - let localCfg = localStorage.getItem('cfg'); - if (localCfg) { - try { - cfg = JSON.parse(localCfg); - } catch (e) { - cfg = {}; - } - } else { - - } -} - -async function getServerEnvVars(){ - await axios.get(`${ENVURL}`).then((res)=>{ - env = res.data; - //log(env); - }).catch((err)=>{ - //log(err); - }); - log('') -} - -function initServices(){ - //connect to websocket server - //grab endpoints from cfg -} - -function initDOM(){ - dom.body = $('body')[0]; -} - -function log(msg, lvl=1){ - if (dom.debugInfo){ - dom.debugInfo.innerHTML = msg; //TODO running log + timestamp - } - console.log(msg); -} \ No newline at end of file diff --git a/web-timeplot/src/test-data-generators.js b/web-timeplot/src/test-data-generators.js deleted file mode 100644 index 02bc0ad..0000000 --- a/web-timeplot/src/test-data-generators.js +++ /dev/null @@ -1,530 +0,0 @@ -/** - * Test Data Generators - Classes for generating fake/test data patterns - * - * These generators produce various types of synthetic data for testing - * and visualizing the waterfall graphs with realistic patterns. - */ - -/** - * Base class for all data generators - */ -class DataGenerator { - constructor(config = {}) { - this.sampleRate = config.sampleRate || 100; // Samples per second - this.amplitude = config.amplitude || 1.0; - this.offset = config.offset || 0.0; - this.time = 0; - } - - /** - * Generate a single sample at the current time - * @returns {number} The generated value - */ - sample() { - throw new Error('sample() must be implemented by subclass'); - } - - /** - * Generate an array of samples - * @param {number} count - Number of samples to generate - * @returns {Array} Array of generated values - */ - generateSamples(count) { - const samples = []; - for (let i = 0; i < count; i++) { - samples.push(this.sample()); - this.time += 1 / this.sampleRate; - } - return samples; - } - - /** - * Generate a line of points for waterfall display - * @param {number} pointCount - Number of points in the line - * @param {number} width - Width of the display area - * @returns {Array<{x: number, y: number}>} Array of points - */ - generateLine(pointCount, width) { - const points = []; - const samples = this.generateSamples(pointCount); - - for (let i = 0; i < pointCount; i++) { - const x = (i / pointCount) * width; - const y = samples[i] * this.amplitude + this.offset; - points.push({ x, y }); - } - - return points; - } - - reset() { - this.time = 0; - } -} - -/** - * Sine Wave Generator - Classic sinusoidal wave - */ -export class SineWaveGenerator extends DataGenerator { - constructor(config = {}) { - super(config); - this.frequency = config.frequency || 1.0; // Hz - this.phase = config.phase || 0.0; // Radians - } - - sample() { - const value = Math.sin(2 * Math.PI * this.frequency * this.time + this.phase); - return value; - } -} - -/** - * Square Wave Generator - Digital-style square wave - */ -export class SquareWaveGenerator extends DataGenerator { - constructor(config = {}) { - super(config); - this.frequency = config.frequency || 1.0; - this.dutyCycle = config.dutyCycle || 0.5; // 0.0 to 1.0 - } - - sample() { - const period = 1 / this.frequency; - const phase = (this.time % period) / period; - return phase < this.dutyCycle ? 1.0 : -1.0; - } -} - -/** - * Sawtooth Wave Generator - Linear ramp wave - */ -export class SawtoothWaveGenerator extends DataGenerator { - constructor(config = {}) { - super(config); - this.frequency = config.frequency || 1.0; - } - - sample() { - const period = 1 / this.frequency; - const phase = (this.time % period) / period; - return 2 * phase - 1; // -1 to 1 - } -} - -/** - * Triangle Wave Generator - Linear up/down wave - */ -export class TriangleWaveGenerator extends DataGenerator { - constructor(config = {}) { - super(config); - this.frequency = config.frequency || 1.0; - } - - sample() { - const period = 1 / this.frequency; - const phase = (this.time % period) / period; - return phase < 0.5 - ? 4 * phase - 1 - : 3 - 4 * phase; - } -} - -/** - * White Noise Generator - Random noise - */ -export class WhiteNoiseGenerator extends DataGenerator { - sample() { - return Math.random() * 2 - 1; // -1 to 1 - } -} - -/** - * Pink Noise Generator - 1/f noise (more realistic than white noise) - */ -export class PinkNoiseGenerator extends DataGenerator { - constructor(config = {}) { - super(config); - // Paul Kellet's refined method - this.b0 = 0; - this.b1 = 0; - this.b2 = 0; - this.b3 = 0; - this.b4 = 0; - this.b5 = 0; - this.b6 = 0; - } - - sample() { - const white = Math.random() * 2 - 1; - this.b0 = 0.99886 * this.b0 + white * 0.0555179; - this.b1 = 0.99332 * this.b1 + white * 0.0750759; - this.b2 = 0.96900 * this.b2 + white * 0.1538520; - this.b3 = 0.86650 * this.b3 + white * 0.3104856; - this.b4 = 0.55000 * this.b4 + white * 0.5329522; - this.b5 = -0.7616 * this.b5 - white * 0.0168980; - const pink = this.b0 + this.b1 + this.b2 + this.b3 + this.b4 + this.b5 + this.b6 + white * 0.5362; - this.b6 = white * 0.115926; - return pink * 0.11; // Normalize - } -} - -/** - * Perlin Noise Generator - Smooth, continuous noise - */ -export class PerlinNoiseGenerator extends DataGenerator { - constructor(config = {}) { - super(config); - this.frequency = config.frequency || 1.0; - this.octaves = config.octaves || 4; - this.persistence = config.persistence || 0.5; - } - - // Simple 1D Perlin-like noise - noise(x) { - const i = Math.floor(x); - const f = x - i; - - // Fade curve - const u = f * f * (3 - 2 * f); - - // Hash function for pseudo-random gradients - const hash = (n) => { - n = (n << 13) ^ n; - return (1.0 - ((n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0); - }; - - return (1 - u) * hash(i) + u * hash(i + 1); - } - - sample() { - let value = 0; - let amplitude = 1; - let frequency = this.frequency; - let maxValue = 0; - - for (let i = 0; i < this.octaves; i++) { - value += this.noise(this.time * frequency) * amplitude; - maxValue += amplitude; - amplitude *= this.persistence; - frequency *= 2; - } - - return value / maxValue; - } -} - -/** - * Pulse/Spike Generator - Random spikes/pulses - */ -export class PulseGenerator extends DataGenerator { - constructor(config = {}) { - super(config); - this.pulseRate = config.pulseRate || 0.05; // Probability per sample - this.pulseWidth = config.pulseWidth || 0.01; // Duration in seconds - this.pulseAmplitude = config.pulseAmplitude || 1.0; - this.currentPulse = null; - } - - sample() { - // Check if we're in a pulse - if (this.currentPulse) { - if (this.time >= this.currentPulse.endTime) { - this.currentPulse = null; - } else { - return this.pulseAmplitude; - } - } - - // Random chance to start new pulse - if (Math.random() < this.pulseRate) { - this.currentPulse = { - startTime: this.time, - endTime: this.time + this.pulseWidth, - }; - return this.pulseAmplitude; - } - - return 0; - } -} - -/** - * Burst Generator - Bursts of activity with quiet periods - */ -export class BurstGenerator extends DataGenerator { - constructor(config = {}) { - super(config); - this.burstDuration = config.burstDuration || 1.0; // Seconds - this.quietDuration = config.quietDuration || 2.0; // Seconds - this.burstFrequency = config.burstFrequency || 5.0; // Hz during burst - this.currentState = 'quiet'; - this.stateStartTime = 0; - } - - sample() { - const elapsed = this.time - this.stateStartTime; - - // State transitions - if (this.currentState === 'quiet' && elapsed >= this.quietDuration) { - this.currentState = 'burst'; - this.stateStartTime = this.time; - } else if (this.currentState === 'burst' && elapsed >= this.burstDuration) { - this.currentState = 'quiet'; - this.stateStartTime = this.time; - } - - // Generate value based on state - if (this.currentState === 'burst') { - return Math.sin(2 * Math.PI * this.burstFrequency * this.time); - } else { - return 0; - } - } -} - -/** - * Chirp Generator - Frequency sweep signal - */ -export class ChirpGenerator extends DataGenerator { - constructor(config = {}) { - super(config); - this.startFreq = config.startFreq || 0.5; // Hz - this.endFreq = config.endFreq || 10.0; // Hz - this.duration = config.duration || 5.0; // Seconds - } - - sample() { - const t = this.time % this.duration; - const progress = t / this.duration; - const freq = this.startFreq + (this.endFreq - this.startFreq) * progress; - return Math.sin(2 * Math.PI * freq * t); - } -} - -/** - * Composite Generator - Combine multiple generators - */ -export class CompositeGenerator extends DataGenerator { - constructor(config = {}) { - super(config); - this.generators = config.generators || []; - this.weights = config.weights || this.generators.map(() => 1.0); - } - - sample() { - let sum = 0; - let weightSum = 0; - - for (let i = 0; i < this.generators.length; i++) { - sum += this.generators[i].sample() * this.weights[i]; - weightSum += this.weights[i]; - } - - return weightSum > 0 ? sum / weightSum : 0; - } - - generateSamples(count) { - const samples = []; - for (let i = 0; i < count; i++) { - samples.push(this.sample()); - // Advance all child generators - this.generators.forEach(gen => gen.time += 1 / gen.sampleRate); - } - return samples; - } -} - -/** - * FM (Frequency Modulation) Generator - One signal modulates another - */ -export class FMGenerator extends DataGenerator { - constructor(config = {}) { - super(config); - this.carrierFreq = config.carrierFreq || 5.0; // Hz - this.modulatorFreq = config.modulatorFreq || 0.5; // Hz - this.modulationIndex = config.modulationIndex || 2.0; - } - - sample() { - const modulator = Math.sin(2 * Math.PI * this.modulatorFreq * this.time); - const instantFreq = this.carrierFreq + this.modulationIndex * modulator; - return Math.sin(2 * Math.PI * instantFreq * this.time); - } -} - -/** - * Exponential Decay Generator - Exponentially decaying signal - */ -export class ExponentialDecayGenerator extends DataGenerator { - constructor(config = {}) { - super(config); - this.decayRate = config.decayRate || 1.0; // 1/seconds - this.oscillationFreq = config.oscillationFreq || 5.0; // Hz - } - - sample() { - const envelope = Math.exp(-this.decayRate * this.time); - const oscillation = Math.sin(2 * Math.PI * this.oscillationFreq * this.time); - return envelope * oscillation; - } -} - -/** - * Step Function Generator - Random walk / brownian motion - */ -export class RandomWalkGenerator extends DataGenerator { - constructor(config = {}) { - super(config); - this.stepSize = config.stepSize || 0.1; - this.currentValue = 0; - this.bounds = config.bounds || { min: -5, max: 5 }; - } - - sample() { - // Random step - const step = (Math.random() - 0.5) * this.stepSize; - this.currentValue += step; - - // Apply bounds - this.currentValue = Math.max(this.bounds.min, Math.min(this.bounds.max, this.currentValue)); - - return this.currentValue; - } -} - -// ============================================================================ -// Example Usage and Presets -// ============================================================================ - -/** - * Factory function to create common test scenarios - */ -export class TestDataFactory { - static createSimpleSine(amplitude = 30) { - return new SineWaveGenerator({ - frequency: 2.0, - amplitude: amplitude, - sampleRate: 100, - }); - } - - static createNoisySine(amplitude = 30) { - const sine = new SineWaveGenerator({ - frequency: 2.0, - amplitude: amplitude * 0.8, - sampleRate: 100, - }); - - const noise = new WhiteNoiseGenerator({ - amplitude: amplitude * 0.2, - sampleRate: 100, - }); - - return new CompositeGenerator({ - generators: [sine, noise], - weights: [1.0, 1.0], - }); - } - - static createComplexPattern(amplitude = 30) { - const low = new SineWaveGenerator({ - frequency: 0.5, - amplitude: amplitude * 0.4, - sampleRate: 100, - }); - - const mid = new SineWaveGenerator({ - frequency: 3.0, - amplitude: amplitude * 0.3, - sampleRate: 100, - }); - - const high = new SineWaveGenerator({ - frequency: 8.0, - amplitude: amplitude * 0.2, - sampleRate: 100, - }); - - const noise = new PinkNoiseGenerator({ - amplitude: amplitude * 0.1, - sampleRate: 100, - }); - - return new CompositeGenerator({ - generators: [low, mid, high, noise], - weights: [1.0, 1.0, 1.0, 1.0], - }); - } - - static createBurstySignal(amplitude = 30) { - return new BurstGenerator({ - amplitude: amplitude, - burstDuration: 0.5, - quietDuration: 1.5, - burstFrequency: 10.0, - sampleRate: 100, - }); - } - - static createSmoothNoise(amplitude = 30) { - return new PerlinNoiseGenerator({ - amplitude: amplitude, - frequency: 2.0, - octaves: 3, - persistence: 0.5, - sampleRate: 100, - }); - } - - static createFrequencySweep(amplitude = 30) { - return new ChirpGenerator({ - amplitude: amplitude, - startFreq: 0.5, - endFreq: 10.0, - duration: 3.0, - sampleRate: 100, - }); - } - - static createModulatedSignal(amplitude = 30) { - return new FMGenerator({ - amplitude: amplitude, - carrierFreq: 5.0, - modulatorFreq: 0.3, - modulationIndex: 3.0, - sampleRate: 100, - }); - } - - static createRandomWalk(amplitude = 30) { - return new RandomWalkGenerator({ - stepSize: 0.5, - bounds: { min: -amplitude, max: amplitude }, - sampleRate: 100, - }); - } -} - -/** - * Example: How to use with WaterfallGraph - * - * // Create a generator - * const generator = TestDataFactory.createComplexPattern(30); - * - * // In your graph's addLine method: - * addLine(time, graphIdx) { - * const line = { - * points: generator.generateLine(this.pointsPerLine, this.width), - * yOffset: 0, - * color: this.generateColor(time), - * }; - * this.lines.push(line); - * } - * - * // Or generate custom samples: - * const samples = generator.generateSamples(100); - * const points = samples.map((y, i) => ({ - * x: (i / samples.length) * width, - * y: y - * })); - */ diff --git a/web-timeplot/src/timeseries-plot.js b/web-timeplot/src/timeseries-plot.js deleted file mode 100644 index e35a704..0000000 --- a/web-timeplot/src/timeseries-plot.js +++ /dev/null @@ -1,277 +0,0 @@ -import { Container, Graphics, Text } from 'pixi.js'; - -/** - * TimeSeriesPlot - Pure visualization component for time-series data - * - * This class is responsible ONLY for displaying data, not generating it. - * It receives data points from external sources and renders them as a - * scrolling waterfall display. - * - * Architecture: - * - TimeSeriesPlot: Displays data (this file) - * - DataSource: Generates/provides data (data-sources.js) - * - Connection: Links sources to plots - */ -export class TimeSeriesPlot { - constructor(config) { - this.x = config.x || 0; - this.y = config.y || 0; - this.width = config.width || 800; - this.height = config.height || 600; - this.title = config.title || 'Time Series'; - this.baseColor = config.color || 0xff6666; - - // Container for all graphics - this.container = new Container(); - this.container.x = this.x; - this.container.y = this.y; - - // Graphics layers (order matters for rendering) - this.gridGraphics = new Graphics(); - this.linesGraphics = new Graphics(); - this.borderGraphics = new Graphics(); - - this.container.addChild(this.gridGraphics); - this.container.addChild(this.linesGraphics); - this.container.addChild(this.borderGraphics); - - // Title - this.titleText = new Text({ - text: this.title, - style: { - fontFamily: 'Arial', - fontSize: 18, - fill: 0xeeeeee, - } - }); - this.titleText.x = 10; - this.titleText.y = 10; - this.container.addChild(this.titleText); - - // Display state - this.lines = []; // Array of {points, yOffset, color, metadata} - this.maxLines = config.maxLines || 100; - this.showGrid = config.showGrid !== false; - - // Scrolling and scaling - this.scrollSpeed = config.scrollSpeed || 1.0; - this.verticalScale = config.verticalScale || 1.0; - - // Initial draw - this.draw(); - } - - // ======================================================================== - // Data Input API - This is how external sources send data to the plot - // ======================================================================== - - /** - * Add a new line of data to the plot - * @param {Array<{x: number, y: number}>} points - Array of points - * @param {Object} metadata - Optional metadata (color, timestamp, etc.) - */ - addLine(points, metadata = {}) { - const line = { - points: points, - yOffset: 0, - color: metadata.color || this.generateColor(Date.now() / 1000), - timestamp: metadata.timestamp || Date.now(), - metadata: metadata, - }; - - this.lines.push(line); - - // Limit number of lines - if (this.lines.length > this.maxLines) { - this.lines.shift(); - } - } - - /** - * Add a single data point (will be buffered into a line) - * This is useful for streaming real-time data - * @param {number} timestamp - Time of the data point - * @param {number} value - Value at this time - */ - addDataPoint(timestamp, value) { - // For now, this creates a single-point line - // In a more sophisticated version, this could buffer points - // until a full line is ready - const point = { x: this.width / 2, y: value }; - this.addLine([point], { timestamp }); - } - - /** - * Clear all data from the plot - */ - clearData() { - this.lines = []; - this.drawLines(); - } - - // ======================================================================== - // Update and Rendering - // ======================================================================== - - /** - * Update the plot - called each frame - * This handles scrolling and cleanup, but NOT data generation - */ - update() { - // Scroll existing lines down - this.scrollLines(); - - // Remove off-screen lines - this.lines = this.lines.filter(line => { - const scaledOffset = line.yOffset * this.verticalScale; - return scaledOffset < this.height + 50; - }); - - // Redraw - this.drawLines(); - } - - scrollLines() { - this.lines.forEach(line => { - line.yOffset += this.scrollSpeed; - }); - } - - draw() { - this.drawBorder(); - this.drawGrid(); - this.drawLines(); - } - - drawBorder() { - this.borderGraphics.clear(); - this.borderGraphics.rect(0, 0, this.width, this.height); - this.borderGraphics.stroke({ width: 2, color: 0x606070 }); - } - - drawGrid() { - this.gridGraphics.clear(); - - if (!this.showGrid) return; - - this.gridGraphics.alpha = 0.3; - - const divisions = 10; - const color = 0x4a7a9a; - - // Vertical lines - for (let i = 0; i <= divisions; i++) { - const x = (i / divisions) * this.width; - this.gridGraphics.moveTo(x, 0); - this.gridGraphics.lineTo(x, this.height); - this.gridGraphics.stroke({ width: 1, color }); - } - - // Horizontal lines - for (let i = 0; i <= divisions; i++) { - const y = (i / divisions) * this.height; - this.gridGraphics.moveTo(0, y); - this.gridGraphics.lineTo(this.width, y); - this.gridGraphics.stroke({ width: 1, color }); - } - } - - drawLines() { - this.linesGraphics.clear(); - - for (const line of this.lines) { - if (line.points.length < 2) continue; - - // Apply vertical scale to y positions - const scaledYOffset = line.yOffset * this.verticalScale; - - // Start path - const firstPoint = line.points[0]; - this.linesGraphics.moveTo(firstPoint.x, firstPoint.y + scaledYOffset); - - // Draw line strip - for (let i = 1; i < line.points.length; i++) { - const point = line.points[i]; - this.linesGraphics.lineTo(point.x, point.y + scaledYOffset); - } - - this.linesGraphics.stroke({ width: 2, color: line.color }); - } - } - - generateColor(time) { - // Cycle through colors based on time - const hue = (time * 0.1) % 1.0; - const r = Math.floor(Math.abs(Math.sin(hue * Math.PI * 2)) * 255); - const g = Math.floor(Math.abs(Math.sin((hue + 0.33) * Math.PI * 2)) * 255); - const b = Math.floor(Math.abs(Math.sin((hue + 0.66) * Math.PI * 2)) * 255); - - return (r << 16) | (g << 8) | b; - } - - // ======================================================================== - // Configuration and Control - // ======================================================================== - - setGridVisible(visible) { - this.showGrid = visible; - this.drawGrid(); - } - - setScrollSpeed(speed) { - this.scrollSpeed = Math.max(0.1, Math.min(10.0, speed)); - } - - setVerticalScale(scale) { - this.verticalScale = Math.max(0.2, Math.min(3.0, scale)); - } - - setTitle(title) { - this.title = title; - this.titleText.text = title; - } - - resize(x, y, width, height) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - - this.container.x = x; - this.container.y = y; - - this.draw(); - } - - // ======================================================================== - // Statistics and Debugging - // ======================================================================== - - getVertexCount() { - return this.lines.reduce((sum, line) => sum + line.points.length, 0); - } - - getLineCount() { - return this.lines.length; - } - - getOldestTimestamp() { - if (this.lines.length === 0) return null; - return Math.min(...this.lines.map(l => l.timestamp)); - } - - getNewestTimestamp() { - if (this.lines.length === 0) return null; - return Math.max(...this.lines.map(l => l.timestamp)); - } - - getStats() { - return { - lineCount: this.getLineCount(), - vertexCount: this.getVertexCount(), - oldestTimestamp: this.getOldestTimestamp(), - newestTimestamp: this.getNewestTimestamp(), - timeSpan: this.getNewestTimestamp() - this.getOldestTimestamp(), - }; - } -} diff --git a/web-timeplot/src/ui/panel-manager.js b/web-timeplot/src/ui/panel-manager.js deleted file mode 100644 index ad29697..0000000 --- a/web-timeplot/src/ui/panel-manager.js +++ /dev/null @@ -1,542 +0,0 @@ -import { formatDuration, formatValue, formatWallClock } from '../utils-format.js'; - -function createElement(tagName, className, textContent) { - const element = document.createElement(tagName); - if (className) { - element.className = className; - } - if (textContent) { - element.textContent = textContent; - } - return element; -} - -function setToggleState(element, active) { - element.dataset.active = String(active); -} - -function readControlValue(element) { - if (element.tagName === 'SELECT') { - return element.value; - } - - if (element instanceof HTMLInputElement) { - if (element.type === 'checkbox') { - return element.checked; - } - - if (element.type === 'number' || element.type === 'range') { - return Number(element.value); - } - - return element.value; - } - - return element.value; -} - -function syncControlValue(element, value) { - if (!element || document.activeElement === element) { - return; - } - - if (element instanceof HTMLInputElement && element.type === 'checkbox') { - element.checked = Boolean(value); - return; - } - - element.value = String(value ?? ''); -} - -export class PanelManager { - constructor({ root, store, actions }) { - this.root = root; - this.store = store; - this.actions = actions; - this.elements = {}; - } - - mount() { - const shell = createElement('div', 'timeplot-shell'); - const topbar = createElement('header', 'timeplot-topbar'); - const viewport = createElement('section', 'timeplot-viewport'); - const plotGrid = createElement('div', 'timeplot-plot-grid'); - const primaryPlotPanel = createElement('section', 'timeplot-plot-panel'); - const secondaryPlotPanel = createElement('section', 'timeplot-plot-panel'); - const primaryCanvasHost = createElement('div', 'timeplot-canvas-host'); - const secondaryCanvasHost = createElement('div', 'timeplot-canvas-host'); - const sidebar = createElement('aside', 'timeplot-sidebar'); - const primaryTooltip = createElement('div', 'timeplot-tooltip'); - const secondaryTooltip = createElement('div', 'timeplot-tooltip'); - primaryTooltip.hidden = true; - secondaryTooltip.hidden = true; - - const brand = createElement('div', 'timeplot-brand'); - const title = createElement('h1', 'timeplot-title', 'TimePlot'); - const subtitle = createElement('div', 'timeplot-subtitle', 'Dual synchronized signal monitor'); - brand.append(title, subtitle); - - const toolbar = createElement('div', 'timeplot-toolbar'); - toolbar.append( - this.createTransportControls(), - this.createPanelToggles(), - ); - - topbar.append(brand, toolbar); - primaryPlotPanel.append(primaryCanvasHost, primaryTooltip); - secondaryPlotPanel.append(secondaryCanvasHost, secondaryTooltip); - plotGrid.append(primaryPlotPanel, secondaryPlotPanel); - viewport.append(plotGrid); - shell.append(topbar, viewport, sidebar); - this.root.replaceChildren(shell); - - this.elements = { - ...this.elements, - shell, - topbar, - viewport, - plotGrid, - primaryPlotPanel, - secondaryPlotPanel, - primaryCanvasHost, - secondaryCanvasHost, - sidebar, - primaryTooltip, - secondaryTooltip, - title, - subtitle, - statusPanel: this.createStatusPanel(), - sourcePanel: this.createSourcePanel(), - configPanel: this.createConfigPanel(), - helpPanel: this.createHelpPanel(), - }; - - sidebar.append( - this.elements.statusPanel, - this.elements.sourcePanel, - this.elements.configPanel, - this.elements.helpPanel, - ); - - return this.elements; - } - - createTransportControls() { - const wrapper = createElement('div', 'control-group'); - const pauseButton = createElement('button', 'control-button', 'Pause'); - const resetButton = createElement('button', 'control-button', 'Reset'); - const speedLabel = createElement('span', null, 'Speed'); - const speedInput = document.createElement('input'); - speedInput.type = 'range'; - speedInput.min = '0.1'; - speedInput.max = '6'; - speedInput.step = '0.1'; - const speedValue = createElement('span', null, '1.0×'); - - pauseButton.addEventListener('click', () => this.actions.togglePause()); - resetButton.addEventListener('click', () => this.actions.resetScene()); - speedInput.addEventListener('input', (event) => this.actions.setSpeed(Number(event.target.value))); - - wrapper.append(pauseButton, resetButton, speedLabel, speedInput, speedValue); - this.elements.pauseButton = pauseButton; - this.elements.resetButton = resetButton; - this.elements.speedInput = speedInput; - this.elements.speedValue = speedValue; - return wrapper; - } - - createPanelToggles() { - const wrapper = createElement('div', 'control-group'); - const panelIds = ['status', 'source', 'config', 'help']; - this.elements.panelButtons = {}; - - for (const panelId of panelIds) { - const button = createElement('button', 'panel-toggle', panelId); - button.addEventListener('click', () => this.actions.togglePanel(panelId)); - this.elements.panelButtons[panelId] = button; - wrapper.append(button); - } - - return wrapper; - } - - createStatusPanel() { - const panel = createElement('section', 'panel'); - panel.innerHTML = ` -

Status

-
-
Renderer
-
Real time
-
Real elapsed
-
Plot time
-
Playback
-
Points
-
- `; - return panel; - } - - createSourcePanel() { - const panel = createElement('section', 'panel'); - panel.innerHTML = ` -

Data Source

-
-
Signal A
-
- -
-
- - - - -
-
- - -
-
-
- - -
-
-
-
-
Signal B
-
- -
-
- - - - -
-
- - -
-
-
- - -
-
-
- `; - - panel.querySelectorAll('[data-source-field]').forEach((input) => { - const eventName = input.tagName === 'SELECT' ? 'change' : 'input'; - input.addEventListener(eventName, () => { - const sourceKey = input.getAttribute('data-source-key'); - const field = input.getAttribute('data-source-field'); - const value = readControlValue(input); - this.actions.updateSource(sourceKey, field, value); - }); - }); - - panel.querySelectorAll('[data-source-file]').forEach((input) => { - input.addEventListener('change', async () => { - const sourceKey = input.getAttribute('data-source-key'); - const file = input.files?.[0]; - if (!file) { - return; - } - - await this.actions.loadSourceFile(sourceKey, file); - input.value = ''; - }); - }); - - return panel; - } - - createConfigPanel() { - const panel = createElement('section', 'panel'); - panel.innerHTML = ` -

Config

-
- - -
- Show grid - -
-
- Show points - -
-
-
-
Graph routing
-
- - - - -
-
- `; - - panel.querySelectorAll('[data-plot-field]').forEach((input) => { - const eventName = input instanceof HTMLInputElement && input.type === 'checkbox' ? 'change' : 'input'; - input.addEventListener(eventName, () => { - const field = input.getAttribute('data-plot-field'); - const value = readControlValue(input); - this.actions.updatePlot(field, value); - }); - }); - - panel.querySelectorAll('[data-graph-field]').forEach((input) => { - input.addEventListener('change', () => { - const graphId = input.getAttribute('data-graph-id'); - const field = input.getAttribute('data-graph-field'); - this.actions.updateGraph(graphId, field, input.value); - }); - }); - - return panel; - } - - createHelpPanel() { - const panel = createElement('section', 'panel'); - panel.innerHTML = ` -

Help

-
    -
  1. Each signal can be synthetic or file-backed CSV replay.
  2. -
  3. Each graph can target Signal A or Signal B independently.
  4. -
  5. Each graph can render raw, delta, or smoothed data.
  6. -
  7. Hover either trace to inspect the nearest synchronized sample.
  8. -
  9. Use pause and speed controls to inspect timing behavior.
  10. -
- `; - return panel; - } - - sync(state, visiblePoints) { - this.elements.title.textContent = state.app.title; - this.elements.subtitle.textContent = 'Dual synchronized signal monitor'; - this.elements.pauseButton.textContent = state.time.paused ? 'Resume' : 'Pause'; - setToggleState(this.elements.pauseButton, state.time.paused); - syncControlValue(this.elements.speedInput, state.time.speed); - this.elements.speedValue.textContent = `${state.time.speed.toFixed(1)}×`; - - const statusFields = this.elements.statusPanel.querySelectorAll('[data-field]'); - const fieldMap = Object.fromEntries(Array.from(statusFields).map((field) => [field.getAttribute('data-field'), field])); - fieldMap.renderer.textContent = state.app.renderer; - fieldMap.realTime.textContent = formatWallClock(state.time.realNowMs); - fieldMap.realElapsed.textContent = formatDuration(state.time.realElapsedMs); - fieldMap.plotTime.textContent = formatDuration(state.time.plotTimeMs); - fieldMap.playback.textContent = state.time.paused ? 'Paused' : `${state.time.speed.toFixed(1)}×`; - fieldMap.points.textContent = typeof visiblePoints === 'object' - ? `${visiblePoints.primary} / ${visiblePoints.secondary}` - : `${visiblePoints}`; - - this.syncSourcePanel(state); - this.syncConfigPanel(state); - this.syncPanels(state); - this.syncTooltip(state); - } - - syncSourcePanel(state) { - Object.entries(state.sources).forEach(([sourceKey, sourceConfig]) => { - syncControlValue(this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="type"]`), sourceConfig.type); - syncControlValue(this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="preset"]`), sourceConfig.preset); - syncControlValue(this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="sampleRateHz"]`), sourceConfig.sampleRateHz); - syncControlValue(this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="amplitude"]`), sourceConfig.amplitude); - syncControlValue(this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="noise"]`), sourceConfig.noise); - const replayRateInput = this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="replayRate"]`); - if (replayRateInput) { - syncControlValue(replayRateInput, sourceConfig.replayRate ?? 1); - } - - const sourceSection = this.elements.sourcePanel.querySelector(`[data-source-config="${sourceKey}"]`); - sourceSection.querySelectorAll('[data-source-mode]').forEach((modeSection) => { - modeSection.hidden = modeSection.getAttribute('data-source-mode') !== sourceConfig.type; - }); - - const meta = this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-meta]`); - if (meta) { - if (sourceConfig.type === 'csv-replay') { - meta.innerHTML = sourceConfig.loadError - ? `${sourceConfig.loadError}` - : `${sourceConfig.dataFileName || 'No file loaded'}${sourceConfig.datasetPointCount ? ` · ${sourceConfig.datasetPointCount} pts · ${formatDuration(sourceConfig.datasetDurationMs || 0)}` : ''}`; - } else if (sourceConfig.type === 'websocket') { - meta.textContent = ''; - } else { - meta.textContent = 'Generates data procedurally in-browser'; - } - } - - const wsUrlInput = this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="wsUrl"]`); - const wsReconnectInput = this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-field="wsReconnectMs"]`); - const wsMeta = this.elements.sourcePanel.querySelector(`[data-source-key="${sourceKey}"][data-source-ws-meta]`); - if (wsUrlInput) { - syncControlValue(wsUrlInput, sourceConfig.wsUrl ?? ''); - } - if (wsReconnectInput) { - syncControlValue(wsReconnectInput, sourceConfig.wsReconnectMs ?? 2000); - } - if (wsMeta) { - wsMeta.innerHTML = sourceConfig.type === 'websocket' - ? `status: ${sourceConfig.wsStatus || 'idle'}${sourceConfig.wsStatusDetail ? ` · ${sourceConfig.wsStatusDetail}` : ''}` - : ''; - } - }); - } - - syncConfigPanel(state) { - syncControlValue(this.elements.configPanel.querySelector('[data-plot-field="windowDurationMs"]'), state.plot.windowDurationMs); - syncControlValue(this.elements.configPanel.querySelector('[data-plot-field="maxPoints"]'), state.plot.maxPoints); - syncControlValue(this.elements.configPanel.querySelector('[data-plot-field="showGrid"]'), state.plot.showGrid); - syncControlValue(this.elements.configPanel.querySelector('[data-plot-field="showPoints"]'), state.plot.showPoints); - syncControlValue(this.elements.configPanel.querySelector('[data-graph-id="primary"][data-graph-field="sourceKey"]'), state.graphs.primary.sourceKey); - syncControlValue(this.elements.configPanel.querySelector('[data-graph-id="primary"][data-graph-field="transform"]'), state.graphs.primary.transform); - syncControlValue(this.elements.configPanel.querySelector('[data-graph-id="secondary"][data-graph-field="sourceKey"]'), state.graphs.secondary.sourceKey); - syncControlValue(this.elements.configPanel.querySelector('[data-graph-id="secondary"][data-graph-field="transform"]'), state.graphs.secondary.transform); - } - - syncPanels(state) { - const panelMap = { - status: this.elements.statusPanel, - source: this.elements.sourcePanel, - config: this.elements.configPanel, - help: this.elements.helpPanel, - }; - - for (const [panelId, panelState] of Object.entries(state.panels)) { - panelMap[panelId].hidden = !panelState.visible; - setToggleState(this.elements.panelButtons[panelId], panelState.visible); - } - } - - syncTooltip(state) { - const tooltipState = state.plot.tooltip; - this.elements.primaryTooltip.hidden = true; - this.elements.secondaryTooltip.hidden = true; - - if (!tooltipState.visible || !tooltipState.point) { - return; - } - - const tooltip = tooltipState.panelId === 'secondary' - ? this.elements.secondaryTooltip - : this.elements.primaryTooltip; - - tooltip.hidden = false; - tooltip.style.left = `${tooltipState.x}px`; - tooltip.style.top = `${tooltipState.y}px`; - tooltip.innerHTML = ` -
Hovered sample
-
Panel${tooltipState.panelLabel ?? 'Primary'}
-
Plot time${formatDuration(tooltipState.point.timeMs)}
-
Value${formatValue(tooltipState.point.value)}
-
Source${tooltipState.point.sourceId}
- ${tooltipState.linkedPoint ? `
Linked panel${tooltipState.linkedPanelLabel ?? 'Linked'}
` : ''} - ${tooltipState.linkedPoint ? `
Linked value${formatValue(tooltipState.linkedPoint.value)}
` : ''} - `; - } -} diff --git a/web-timeplot/src/utils-format.js b/web-timeplot/src/utils-format.js deleted file mode 100644 index f4eac88..0000000 --- a/web-timeplot/src/utils-format.js +++ /dev/null @@ -1,22 +0,0 @@ -export function formatDuration(ms) { - const totalSeconds = Math.max(0, ms / 1000); - if (totalSeconds < 60) { - return `${totalSeconds.toFixed(2)}s`; - } - - const minutes = Math.floor(totalSeconds / 60); - const seconds = totalSeconds % 60; - return `${minutes}m ${seconds.toFixed(1)}s`; -} - -export function formatWallClock(timestampMs) { - return new Intl.DateTimeFormat(undefined, { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }).format(new Date(timestampMs)); -} - -export function formatValue(value) { - return Number.isFinite(value) ? value.toFixed(3) : '—'; -} diff --git a/web-timeplot/src/waterfall.js b/web-timeplot/src/waterfall.js deleted file mode 100644 index bce0750..0000000 --- a/web-timeplot/src/waterfall.js +++ /dev/null @@ -1,219 +0,0 @@ -import { Container, Graphics, Text } from 'pixi.js'; - -/** - * WaterfallGraph - A scrolling waterfall display - * Starts simple with basic line rendering - */ -export class WaterfallGraph { - constructor(config) { - this.x = config.x; - this.y = config.y; - this.width = config.width; - this.height = config.height; - this.title = config.title; - this.baseColor = config.color || 0xff6666; - - this.container = new Container(); - this.container.x = this.x; - this.container.y = this.y; - - // Graphics layers - this.borderGraphics = new Graphics(); - this.gridGraphics = new Graphics(); - this.linesGraphics = new Graphics(); - - this.container.addChild(this.gridGraphics); - this.container.addChild(this.linesGraphics); - this.container.addChild(this.borderGraphics); - - // Title text - this.titleText = new Text({ - text: this.title, - style: { - fontFamily: 'Arial', - fontSize: 18, - fill: 0xeeeeee, - } - }); - this.titleText.x = 10; - this.titleText.y = 10; - this.container.addChild(this.titleText); - - // Waterfall data - this.lines = []; - this.maxLines = 50; - this.pointsPerLine = 100; - this.frameCounter = 0; - - this.showGrid = true; - - // Time scaling and zoom - this.scrollSpeed = 1.0; // Speed multiplier for scrolling - this.baseScrollSpeed = 1.0; - this.verticalScale = 1.0; // Vertical zoom: >1 = zoomed in (see less history), <1 = zoomed out (see more) - - this.draw(); - } - - draw() { - this.drawBorder(); - this.drawGrid(); - } - - drawBorder() { - this.borderGraphics.clear(); - this.borderGraphics.rect(0, 0, this.width, this.height); - this.borderGraphics.stroke({ width: 2, color: 0x606070 }); - } - - drawGrid() { - this.gridGraphics.clear(); - - if (!this.showGrid) return; - - this.gridGraphics.alpha = 0.3; - - const divisions = 10; - const color = 0x4a7a9a; - - // Vertical lines - for (let i = 0; i <= divisions; i++) { - const x = (i / divisions) * this.width; - this.gridGraphics.moveTo(x, 0); - this.gridGraphics.lineTo(x, this.height); - this.gridGraphics.stroke({ width: 1, color }); - } - - // Horizontal lines - for (let i = 0; i <= divisions; i++) { - const y = (i / divisions) * this.height; - this.gridGraphics.moveTo(0, y); - this.gridGraphics.lineTo(this.width, y); - this.gridGraphics.stroke({ width: 1, color }); - } - } - - update(time, graphIdx) { - this.frameCounter++; - - // Add new line every 10 frames - if (this.frameCounter % 10 === 0 && this.lines.length < this.maxLines) { - this.addLine(time, graphIdx); - } - - // Scroll existing lines down - this.scrollLines(); - - // Remove off-screen lines - this.lines = this.lines.filter(line => line.yOffset < this.height + 50); - - // Redraw all lines - this.drawLines(); - } - - addLine(time, graphIdx) { - const line = { - points: [], - yOffset: 0, - color: this.generateColor(time), - }; - - // Generate sine wave points - const phase = time + (graphIdx * 2); - const freq = 2.0 + Math.sin(time * 0.5 + graphIdx) * 1.0; - - for (let i = 0; i < this.pointsPerLine; i++) { - const x = (i / this.pointsPerLine) * this.width; - const normalizedX = (i / this.pointsPerLine) * 2 - 1; // -1 to 1 - const y = Math.sin(i * 0.1 * freq + phase) * 30; // Amplitude in pixels - - line.points.push({ x, y }); - } - - this.lines.push(line); - } - - scrollLines() { - const speed = this.baseScrollSpeed * this.scrollSpeed; - this.lines.forEach(line => { - line.yOffset += speed; - }); - } - - setScrollSpeed(speed) { - // Clamp between 0.1 (slow) and 5.0 (fast) - this.scrollSpeed = Math.max(0.1, Math.min(5.0, speed)); - } - - getScrollSpeed() { - return this.scrollSpeed; - } - - setVerticalScale(scale) { - // Clamp between 0.2 (zoomed out, see more history) and 3.0 (zoomed in, see less) - this.verticalScale = Math.max(0.2, Math.min(3.0, scale)); - } - - getVerticalScale() { - return this.verticalScale; - } - - drawLines() { - this.linesGraphics.clear(); - - for (const line of this.lines) { - if (line.points.length < 2) continue; - - // Apply vertical scale to y positions - // Current time is at top (y=0), older data has larger yOffset - const scaledYOffset = line.yOffset * this.verticalScale; - - // Start path - const firstPoint = line.points[0]; - this.linesGraphics.moveTo(firstPoint.x, firstPoint.y + scaledYOffset); - - // Draw line strip - for (let i = 1; i < line.points.length; i++) { - const point = line.points[i]; - this.linesGraphics.lineTo(point.x, point.y + scaledYOffset); - } - - this.linesGraphics.stroke({ width: 2, color: line.color }); - } - } - - generateColor(time) { - // Cycle through colors based on time - const hue = (time * 0.1) % 1.0; - const r = Math.floor(Math.abs(Math.sin(hue * Math.PI * 2)) * 255); - const g = Math.floor(Math.abs(Math.sin((hue + 0.33) * Math.PI * 2)) * 255); - const b = Math.floor(Math.abs(Math.sin((hue + 0.66) * Math.PI * 2)) * 255); - - return (r << 16) | (g << 8) | b; - } - - setGridVisible(visible) { - this.showGrid = visible; - this.drawGrid(); - } - - resize(x, y, width, height) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - - this.container.x = x; - this.container.y = y; - - this.draw(); - } - - getVertexCount() { - return this.lines.reduce((sum, line) => sum + line.points.length, 0); - } - - getLineCount() { - return this.lines.length; - } -} -- cgit v1.2.3