@myrmidon/gve-snapshot-view
v1.1.6
Published
GVE snapshot view
Downloads
351
Readme
GVE Snapshot View Web Component
This project contains the GVE snapshot view web component, which is a pure Typescript, JS-framework independent component to visualize a GVE snapshot with SVG.
🚀 Quick start:
npm i
to install packages.npm run build
to build.npm run start
to start the test page. Here you should paste the JSON code representing a snapshot, and click the button to view it. You can copy an example JSON code from arzdc.json, which is not used by the code but is a resource available to the user for this purpose. You can also use abcxy.json to test for automatic positioning with multiple "layers" as derived from nodes added by operations.
API
This component acts as a black box, receiving a snapshot object of type Snapshot
, and rendering it into its svg
element, according to the specified options and snapshot data.
- 📦 install:
npm i @myrmidon/gve-snapshot-view
- 🔑 selector:
gve-snapshot-view
- input:
- ▶️
data
(SnapshotViewData | undefined | null
): the snapshot view data to render:snapshot
(Snapshot
):size
(Size
): the SVG size.width
(number
)height
(number
)
style
(string | undefined
): the SVG style.defs
(string | undefined
): the optional SVGdefs
code to be used in the view.image
(SnapshotImage | undefined
): the optional background image:url
(string
)canvas
(Rectangle
): the optional canvas to fit the image into. If not specified, the image will have the same size of the SVG container, and placed at its top-left corner (0,0):x
(number
)y
(number
)width
(number
)height
(number
)
opacity
(number
): the default background image opacity. If not specified this is equal to 0. So, to show the image you should specify an opacity value here, or set it later via the renderer'ssetImageOpacity
method.
text
(string | CharNode[]
): the base text. This can be a string, which will be converted into an array of nodes, or directly an array of nodes.id
(number
)index
(number
)label
(string
)data
(string
)sourceTag
: (string | undefined
)features
(Feature[] | undefined
):name
(string
)value
(string
)setPolicy
(number)
textStyle
(string | undefined
): the style for the textg
element.textOptions
(SvgBaseTextOptions
):lineHeightOffset
(number
): offset to add to each calculated line height.charSpacing
(number
): offset to add to each calculated character width.spcWidthOffset
(number
): offset to add to each calculated space width.offset
(Point
): offset for the text origin.x
(number
)y
(number
)
minLineHeights
(Record<number, number>
): minimum line heights for each line number (1-N). You can specify a minimum line height for each line number to ensure that its height is at least the specified value. This is useful for making room between lines for annotations that may exceed the base text.operations
(CharChainOperation[]
):id
(string
)rank
(number | undefined
)groupId
(string | undefined
)features
(OperationFeature[] | undefined
):name
(string
)value
(string
)setPolicy
(number)isNegated
(boolean | undefined
)isGlobal
(boolean | undefined
)isShortLived
(boolean | undefined
)
sources
(OperationSource[] | undefined
)diplomatics
(OperationDiplomatics | undefined
):g
(string
)isNewTextHidden
(boolean | undefined
)features
(Feature[] | undefined
):name
(string
)value
(string
)setPolicy
(FeatureSetPolicy
)
elementFeatures
(Record<string, Feature[]>
)
type
(OperationType
)at
(number
)atAsIndex
(boolean | undefined
)to
(number | undefined
): to coordinate, for move/swap only.toAsIndex
(boolean | undefined
)inputTag
(string | undefined
)outputTag
(string | undefined
)run
(number
)toRun
(number | undefined
)value
(string | undefined
)
opStyle
(string
): the style for the operationsg
element.timelines
(Record<string, GveAnimationTimeline>
):tag
(string
)tweens
(GveAnimationTween[]
):label
(string
)note
(string | undefined
)type
(string
);selector
(string
)vars
(string | undefined
)vars2
(string | undefined
)position
(string | undefined
)
vars
(GveAnimationVars | undefined
)
options
(SnapshotViewOptions | undefined
):hideBase
(boolean | undefined
): true to hide the base text.hideOperations
(boolean | undefined
): true to hide the operations.operationIds
(string[] | undefined
): the IDs of the operations to show; all the other operations will be hidden by setting their opacity to 0.showRulers
(boolean | undefined
): true to show rulers.showGrid
(boolean | undefined
): true to show gridlines on the rulers.rulerColor
(boolean | undefined
): the rulers and gridlines color (default#aaa
).debug
(boolean | undefined
): true to enable debug mode.delayedRender
(boolean | undefined
): true to delay the positioning of rendered text elements so that position is calculated after they are rendered in the DOM. This might be useful in some environments like Angular.panZoom
(boolean | undefined
): true to enable pan and zoom functionality.
- ▶️
- output:
- 🔥
snapshotRender
: fired when the snapshot is rendered;detail
(SnapshotViewRenderEvent
) has the root SVG element and the renderer service:svg
(SVGSVGElement
)renderer
(SnapshotViewService
):characters
(CharNode[]
)visuals
(GveVisual[]
):id
(string
)type
(string
)style
(string | undefined
)class
(string | undefined
)data
(any | undefined
)element
(SVGElement | undefined
)attributes
(Record<string, string> | undefined
): not usedhandlers
(object | undefined
)
- 🔥
visualEvent
: fired when the user interacts with any rendered visual with the mouse;detail
(SnapshotViewVisualEvent
) has the mouse event and the source visual object (GveVisual
):event
(MouseEvent
)source
(GveVisual
)
- 🔥
GVE-related models are currently defined in this library for convenience (src/models). Once they reach stability, they will be moved into an independent library (
gve-snapshot-core
) which will thus be shared among a wider range of frontend components dealing with snapshots visualization.
Usage
Example usage:
// getting custom component
let component = document.querySelector('gve-snapshot-view');
// setting input data
component.data = data;
// listening to events
component.addEventListener("snapshotRender", (event) => {
console.log("Snapshot rendered:", event.detail);
});
component.addEventListener("visualEvent", (event) => {
console.log("Visual event:", event.detail);
});
The component renders a snapshot in this starting template:
<svg id="snapshot">
<!-- background image -->
<image id="image" x="0" y="0" width="0" height="0"></image>
<!-- rulers -->
<g id="rulers"></g>
<!-- base text -->
<g id="base"></g>
<!-- operations -->
<g id="operations"></g>
</svg>
According to data and options, the component will add new SVG elements as descendants of these g
elements.
Element g#base
contains multiline text, where each character (except for newline) is represented by an SVG text
element. In most cases, the characters are displayed one after another on a line (until any newline is found), except for those character having a manually set position (as defined by the x
and y
features of the source model for each character).
To this end, the text renderer must get the bounding box of each character added and use it to determine the position of the next one. In some environments, like Angular, this does not work because there is some delay (probably due to zone.js
) between the addition of the SVG element to the DOM and its effective rendition.
To work around this issue the component has a special rendition mode, enabled by setting its data
's delayedRender
setting to true. When this happens, the component behaves as follows:
- each
text
element gets added and measured, but usually this measurement ends up with a 0-sized bounding box. Anyway, this will not be an issue here becausetext
opacity is set to 0, which ensures that nothing is displayed at this stage (which would result in overwriting all the characters at the same position). The originalopacity
value, when set (and when different from1
), is preserved for a later restore. - once all the elements have been added, inside the handler of
requestAnimationFrame
all thetext
elements corresponding to character and their visuals are updated after querying eachtext
element's bounding box. Also, the element'sopacity
is restored.
If instead delayedRender
is disabled, the position is set immediately after each SVG text
is added, without resetting its opacity
to 0, and this already is the correct position.
Services
Snapshot View Service
The snapshot view service is the engine behind the snapshot view component, used to create all its SVG elements except for those belonging to its main template. This is just a POJO class, which must be simply instantiated and used, and maintains the state for the snapshot viewer.
The service can be accessed on rendition, and provides this API:
- ▶️
characters
(CharNode[]
): the base text characters of the snapshot. - ▶️
visuals
(GveVisual[]
): the viewmodels of visuals rendered by the service. - 🔵
translateSpecialChar
: translate an ASCII special character to a printable character. - 🔵
stringToBaseChars
: convert a string to the character nodes of a base text. - 🔵
addEventHandlers
: add event listeners to a visual. - 🔵
removeEventHandlers
: remove event listeners from a visual. - 🔵
attachEventHandlers
: attach or detach event handlers to the rendered visuals. - 🔵
toggleRulers
: toggle the rulers on (passtrue
) or off (passfalse
), or just toggle the existing state (do not pass anything). Note that if rulers were not rendered by setting the corresponding options, this method will not have any effect. - 🔵
setImageOpacity
: set the opacity of the background image element, if any. - 🔵
render
: render the specified snapshot. - 🔵
playTimeline
: play the specified timeline. - 🔵
pauseTimeline
: pause the specified timeline. - 🔵
resumeTimeline
: resume the specified timeline. - 🔵
reverseTimeline
: play the specified timeline in reverse.
The main method in the service is render
, which gets:
- the snapshot (
Snapshot
) to render; - the rendering options (
SnapshotViewOptions
); - the optional event handlers (
GveVisualEventHandlers
) for the rendered visuals.
The service maintains its rendition in these SVG elements:
- the root SVG
svg
element. This is injected in the service constructor, and it's assumed to have children elements for image, rulers, base, and operations, with their corresponding identifiers. - a background SVG
image
element with IDimage
. - the rulers SVG
g
element with IDrulers
. - the base text SVG
g
element with IDbase
. - the operations root element with ID
operations
.
It also maintains its logical state in:
- an array of
CharNode
items (characters
property): the viewmodels of the rendered characters. - an array of
GveVisual
items (visuals
property): the viewmodels of the rendered visuals.
The rendition is as follows:
set the attributes related to the SVG container as a whole:
- width and height.
- style.
- background image with its X, Y, width, height.
- base text style for the text root element.
- operations style for the operations root element.
if there are any
defs
defined, add them inside a newly createddefs
element, then appended to thesvg
element.if required, create the rulers by adding SVG elements to the rulers
g
group. You can later toggle rulers off and on usingtoggleRulers
, which just changes the rulersg
elementdisplay
style accordingly. So if you want rulers, specify this in the options or they will not be created and toggle will have no effect.render the base text (delayed if requested): this gets the characters from the snapshot (or builds them if the snapshot text is just a string rather than an array of
CharNode
's ), and renders them on 1 or more lines (every\n
character ends a line). If the character hasx
andy
among its features, these will be used as its coordinates, instead of the automatically calculated ones. Each character produces:- an SVG text appended to the text
g
element, with attributesid
=c_
+ character ID (=its ordinal number),x
,y
,dominant-baseline
=hanging
; if using delayed rendering, the text is hidden withopacity
=0 to be shown later in the rendition process. If the character node has features for style and classes, the corresponding attributes are added too. - a visual of type
GveTextVisual
added to thevisuals
property. This has ID=c_<ID>
, and containsx
,y
,type
=char
,width
,height
,lineNr
=current line number,data
=the sourceCharNode
object, andelement
=the SVG text element created for the character. Optionally it hasstyle
andclass
if set from features. If requested, the visual also gets event listeners added.
- an SVG text appended to the text
render operation visuals: each operation having a visual rendition produces:
- a new
g
element as defined by the operation SVGg
model. This element is added to the operations group element. The operation element also gets anopacity
=0 attribute if requested (you can specify a list of transparent operations via the snapshot viewtransparentIds
option). - a visual of type
GveVisual
added to thevisuals
property. This has ID = operation's ID, and containsx
,y
,width
,height
,type
=g
, data=the sourceCharChainOperation
object, andelement
=the SVGg
element created for the operation. Optionally it hasstyle
andclass
if set from features. If requested, the visual also gets event listeners added.
- a new
if delayed rendering is enabled, reposition text elements after measuring them and make them visible.
add pan and zoom support if requested.
Popup Service
You can use the PopupService
to open/close a popup. In this case, you will need to include the following CSS:
.popup {
position: fixed;
top: 0;
left: 0;
background-color: white;
padding: 10px;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
z-index: 10;
}
@keyframes openPopup {
0% {
opacity: 0;
transform: scale(0.5);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes closePopup {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.5);
}
}
.popup-open {
animation: openPopup 0.3s forwards;
}
.popup-close {
animation: closePopup 0.3s forwards;
}
ParsedXmlTag
This utility class represents an XML tag as parsed from a string. It can be used to manage its attributes (read, add, or update them), which can be useful when manipulating SVG code.
- 🔄
name
(string): the tag name. - 🔄
type
(string): the tag type (open, close, empty). - 🔄
index
: the index of the first character of the tag (<
) in the source XML string. - 🔄
length
: the length of the tag in the source XML string, starting fromindex
. - 🔄
attributes
: the attributes in the tag:- 🔄
name
(string): the name. - 🔄
value
(string): the value. - 🔄
index
: the index of the first character of the attribute's name in the source XML string. - 🔄
length
: the length of the attribute in the source XML string, starting from its name and including its value if any. - 🔄
singleQuote
: true if the attribute value was wrapped in single quotes rather than in double quotes in the source XML string.
- 🔄
- 🔵
parse
(static): parse an XML tag from a string at the specified index. - 🔵
toString
: build an XML tag from the specified tag information, overriding the specified attributes values.
Workspace Template
These are the steps for building the template for a pure JS library in VSCode using Typescript, JEST testing, code maps, and webpack bundling like that used here:
create a new folder.
enter it and run
npm init
.install these packages:
npm i --save-dev source-map-loader ts-loader lite-server concurrency npm i --save-dev jest ts-jest @types/jest npm i --save-dev rollup rollup-plugin-terser @rollup/plugin-typescript @rollup/plugin-commonjs @rollup/plugin-node-resolve --force
change
package.json
to add thescripts
,exports
,main
,dependencies
, andtypes
, like in the portion of its code sampled here:{ "main": "./dist/index.js", "type": "module", "types": "./dist/index.d.ts", "scripts": { "build": "rollup -c", "watch": "rollup -c -w", "start": "concurrently \"rollup -c -w\" \"lite-server\"", "test": "jest" }, "exports": { ".": "./dist/index.js" }, "dependencies": { "typescript": "^5.4.5" } }
add
tsconfig.json
for TS configuration:{ "compilerOptions": { "target": "es6", "module": "es6", "moduleResolution": "node", "lib": ["dom", "es2015"], "declaration": true, "outDir": "./dist", "strict": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true }, "include": ["src"], "exclude": ["node_modules", "dist"] }
add
rollup.config.mjs
for bundling:import commonjs from "@rollup/plugin-commonjs"; import typescript from "@rollup/plugin-typescript"; import { terser } from "rollup-plugin-terser"; // Optional: Minification (for production) import { nodeResolve } from "@rollup/plugin-node-resolve"; export default { input: "src/index.ts", // Entry point for your library output: [ { file: "dist/index.js", // Output file for ES6 modules format: "es", }, { file: "dist/index.cjs.min.js", // Output file for CommonJS (minified) format: "cjs", sourcemap: true, plugins: [terser()], // Minify for production }, ], plugins: [ typescript({ tsconfig: "./tsconfig.json" }), // Use your existing tsconfig.json commonjs(), // Convert CommonJS modules to ES6 (for GSAP), nodeResolve(), // Resolve node_modules ], };
add
jest.config.js
for testing:module.exports = { preset: "ts-jest", testEnvironment: "node", };
create in
.vscode
launch.json for debugging:{ "version": "0.2.0", "configurations": [ { "type": "chrome", "request": "launch", "name": "Launch Chrome against localhost", "url": "http://localhost:3000", "webRoot": "${workspaceFolder}" } ] }
add index.html and its consumer.js script to the root to host your controls for testing. This should:
- load your bundled library:
<script type="module" src="./dist/bundle.js"></script>
; - load the test consumer code:
<script type="module" src="consumer.js"></script>
; - use the web component by just placing its element in the page.
- the test JS code must be inside
document.addEventListener("DOMContentLoaded", (event) => { });
to allow for the web component to load before interacting with it.
- load your bundled library:
add your code (services and components) under
src
.export all the required objects from the
src/index.ts
API entrypoint.add
.npmignore
with a content like this:node_modules src *.md *.test.ts *.spec.ts *.spec.d.ts tsconfig.json package-lock.json *.log *.env .vscode consumer.js index.html jest.config.cjs rollup.config.mjs
⚠️ Adding this is essential for NPM, otherwise it will use
.gitignore
for creating a package, which will result in an unusable package including just the sources, while we want the inverse. You can usenpm pack
to create a.tz
file with the package to check its content before publishing.
You can now use:
npm run build
to build.npm run start
to launch the server with the test page.npm run watch
to start and watch for changes.npm publish
to publish the package. Be sure to update the version in package.json as required before publishing.
Note on Lite Server
The lite-server
does not automatically rebuild your TypeScript files when they change. It only refreshes the browser when HTML or JavaScript files change.
To have your TypeScript files automatically recompile when you make changes, you can use tsc --watch
command. This command starts the TypeScript compiler in watch mode; the compiler watches for file changes and recompiles when it sees them.
Then, you can run npm run watch
in a separate terminal to start the TypeScript compiler in watch mode.
However, this will not refresh your browser when your TypeScript files are recompiled. To do this, you can use a tool like concurrently
to run both lite-server
and tsc --watch
at the same time, and have lite-server
refresh the browser whenever your compiled JavaScript files change.
Once installed this Update your start script to run both commands:
"scripts": {
"start": "concurrently \"tsc --watch\" \"lite-server\""
}
Now, when you run npm start
, it will start both the TypeScript compiler in watch mode and lite-server. When you make changes to your TypeScript files, they will be automatically recompiled, and lite-server
will refresh your browser.