@myrmidon/gve-snapshot-view v1.2.0
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.
Workspace
š Quick start to setup this workspace:
npm ito install packages.npm run buildto build.npm run startto 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.
To publish the library package use
npm run publish.
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 SVGdefscode 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'ssetImageOpacitymethod. -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 textgelement. -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. -charDecorator(CharDecoration): an optional function used to override the decoration of each rendered character. The function receives parameterschar(CharNode),ln,x,y(allnumber), and returns either null or an object with any combination of these properties:fill,stroke,strokeWidth,opacity,x,y(where all the properties are numbers except forfillandstroke). This is typically used to leverage trace features in characters and change the character's fill and stroke accordingly. See the test page for an example. -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 operationsgelement. -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);
});SVG Output
āļø 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
textelement gets added and measured, but usually this measurement ends up with a 0-sized bounding box. Anyway, this will not be an issue here becausetextopacity 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 originalopacityvalue, when set (and when different from1), is preserved for a later restore. - once all the elements have been added, inside the handler of
requestAnimationFrameall thetextelements corresponding to character and their visuals are updated after querying eachtextelement's bounding box. Also, the element'sopacityis 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
svgelement. 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
imageelement with IDimage. - the rulers SVG
gelement with IDrulers. - the base text SVG
gelement with IDbase. - the operations root element with ID
operations.
It also maintains its logical state in:
- an array of
CharNodeitems (charactersproperty): the viewmodels of the rendered characters. - an array of
GveVisualitems (visualsproperty): 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
defsdefined, add them inside a newly createddefselement, then appended to thesvgelement.if required, create the rulers by adding SVG elements to the rulers
ggroup. You can later toggle rulers off and on usingtoggleRulers, which just changes the rulersgelementdisplaystyle 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\ncharacter ends a line). If the character hasxandyamong 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
gelement, 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
GveTextVisualadded to thevisualsproperty. This has ID=c_<ID>, and containsx,y,type=char,width,height,lineNr=current line number,data=the sourceCharNodeobject, andelement=the SVG text element created for the character. Optionally it hasstyleandclassif 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
gelement as defined by the operation SVGgmodel. 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 viewtransparentIdsoption). - a visual of type
GveVisualadded to thevisualsproperty. This has ID = operation's ID, and containsx,y,width,height,type=g, data=the sourceCharChainOperationobject, andelement=the SVGgelement created for the operation. Optionally it hasstyleandclassif 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 --forcechange
package.jsonto 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.jsonfor 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.mjsfor 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.jsfor testing:module.exports = { preset: "ts-jest", testEnvironment: "node", };create in
.vscodelaunch.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.tsAPI entrypoint.add
.npmignorewith 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
.gitignorefor creating a package, which will result in an unusable package including just the sources, while we want the inverse. You can usenpm packto create a.tzfile with the package to check its content before publishing.
You can now use:
npm run buildto build.npm run startto launch the server with the test page.npm run watchto start and watch for changes.npm publishto 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.
7 months ago
12 months ago
12 months ago
11 months ago
11 months ago
11 months ago
11 months ago
12 months ago
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago