diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..9772b939 --- /dev/null +++ b/.babelrc @@ -0,0 +1,13 @@ +{ + "presets": [ + [ + "env", + { + "modules": false + } + ] + ], + "plugins": [ + "external-helpers" + ] +} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..32420d72 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,31 @@ +node_modules + +dist +docs/jsdoc + +svgedit-config-es.js +svgedit-config-iife.js +svgedit-custom.css +editor/xdomain-svgedit-config-iife.js + +# Vendor/minified files +editor/jquery.min.js +editor/jquery-ui + +# Previously minified though exporting +editor/jquerybbq + +# Previously minified though exporting +editor/js-hotkeys + +editor/jspdf/jspdf.min.js +editor/jspdf/underscore-min.js + +editor/extensions/imagelib/jquery.min.js +editor/extensions/mathjax + +editor/external/* +!editor/external/dom-polyfill +editor/external/dom-polyfill/* +!editor/external/dom-polyfill/dom-polyfill.js +!editor/external/dynamic-import-polyfill diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..cb7ad48d --- /dev/null +++ b/.eslintrc @@ -0,0 +1,22 @@ +{ + "extends": ["standard", "plugin:qunit/recommended"], + "parserOptions": { + "sourceType": "module" + }, + "plugins": ["qunit"], + "env": { + "node": false, + "browser": true + }, + "rules": { + "semi": [2, "always"], + "indent": ["error", 2, {"outerIIFEBody": 0}], + "no-tabs": 0, + "object-property-newline": 0, + "one-var": 0, + "no-var": 2, + "prefer-const": 2, + "no-extra-semi": 2, + "quote-props": [2, "as-needed"] + } +} diff --git a/.gitignore b/.gitignore index d3638e5f..3c2b54e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ -# See editor/config-sample.js for an example -editor/config.js -editor/custom.css +node_modules + build/ -editor/svgedit.compiled.js + +svgedit-custom.css + +docs/jsdoc diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..ceaef224 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +screencasts +test diff --git a/.remarkrc b/.remarkrc new file mode 100644 index 00000000..bfbd6972 --- /dev/null +++ b/.remarkrc @@ -0,0 +1,10 @@ +{ + "plugins": { + "lint": { + "ordered-list-marker-value": "one", + "no-missing-blank-lines": false, + "list-item-spacing": false, + "list-item-indent": false + } + } +} diff --git a/AUTHORS b/AUTHORS index 3c527e0c..9ea98d20 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,6 +3,7 @@ Pavol Rusnak Jeff Schiller Vidar Hokstad Alexis Deveria +Brett Zamir Translation credits: diff --git a/CHANGES.md b/CHANGES.md index 593a7d53..578a646c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,397 @@ +# 3.0.0-rc.2 + +- Fix: Avoid extension `includeWith` button conflicts/redundancies; + Incorporates #147 +- Fix: Ensure shift-key cycling through flyouts works with extension-added + `includeWith` as well as toolbarbuttons +- Fix: Apply flyout arrows after extensions callback +- Fix: Ensure SVG icon of flyout right-arrow is cloned to can be applied to + more than one extension +- Fix: Ensure line tool shows as selected when "L" key command is used +- Fix: Add images (and references) for fallback (#135) +- Fix (svgIcons plugin): Race condition +- Fix (canvg): Regression for `text` and `tspan` elements as far as + `captureTextNodes` with canvg (inheriting class had set + `captureTextNodes` too late) +- Fix (canvg): Regression on blur +- Fix (canvg): Avoid errors for `tspan` passed to `getGradient` +- i18n: picking stroke/fill paint and opacity +- i18n: Remove eyedropper and imagelib references from main locale (in + extension locale now) +- i18n: Add placeholders for `pick_stroke_paint_opacity`, + `pick_fill_paint_opacity`, `popupWindowBlocked` +- i18n: Update `saveFromBrowser` +- i18n: Reapply locale strings +- Enhancement: Create xdomain file build which works without ES6 Modules +- Enhancement: Build xdomain files dynamically +- Optimize: Further image optimizing +- Optimize: Avoid rewriting `points` attribute for free-hand path; + incorporates #176 (fixes #175) +- Refactoring: Avoid passing on `undefined` var. (#147) +- Refactoring: lbs; avoid indent in connector, destructuring, use map + over push +- Docs: Clarify nature of fixes +- Docs: JSDoc for `setupFlyouts`, `Actions`, `toggleSidePanel`; missing for + ToolbarButton + +# 3.0.0-rc.1 + +- Security fix: 'extPath', 'imgPath', 'extIconsPath', 'canvgPath', + 'langPath', 'jGraduatePath', and 'jspdfPath' were not being prevented +- Breaking change: Rename "svgutils.js" to "utilities.js" (make in + conformity with JSDoc module naming convention) +- Breaking change: Rename "svgedit.js" to "namespaces.js" (to make clear + purpose and avoid confusing with editor) +- Breaking change: Rename "jquery-svg.js" to "jQuery.attr.js" +- Breaking change: Rename "jquery.contextMenu.js" to "jQuery.contextMenu.js" +- Breaking change: Rename "jquery.jpicker.js" to "jQuery.jPicker.js" +- Breaking change: Rename "JQuerySpinBtn.css" to "jQuery.SpinButton.css" +- Breaking change: Rename "JQuerySpinBtn.js" to "jQuery.SpinButton.js" (to + have file name more closely reflect name) +- Breaking change: Rename "jquery.svgicons.js" to "jQuery.svgIcons.js" +- Breaking change: Rename "jquery.jgraduate.js" to "jQuery.jGraduate.js" +- Breaking change: Rename "pathseg.js" to "svgpathseg.js" (as it is a + poyfill of SVGPathSeg) +- Breaking change: Rename `addSvgElementFromJson()` to `addSVGElementFromJson` + for consistency +- Breaking change: Rename `changeSvgContent()` to `changeSVGContent()` for + consistency +- Breaking change: Have `exportPDF` resolve with `output` and `outputType` + rather than `dataurlstring` (as type may vary) +- Breaking change: Rename `extensions/mathjax/MathJax.js` to + `extensions/mathjax/MathJax.min.js` +- Breaking change: Avoid recent change to have editor ready callbacks + return Promises (we're not using and advantageous to keep sequential) +- Breaking change: Avoid recent addition of locale-side function in + ext-imagelib for l10n +- Breaking change: Change name of `ext-arrows.js` from `Arrows` to `arrows` + for sake of file path (not localized anyways). +- Breaking change: Change `addlangData` extension event to `addLangData` + for consistency with method name +- Breaking change: In interests of modularity/removing globals, + remove `window.svgCanvas` and `svgCanvas.ready` as used by older + extensions; use `svgEditor.canvas` and `svgEditor.ready` instead +- Breaking change: Extension now formatted as export (and `this` + is set to editor, including for `callback`) +- Breaking change: Locale now formatted as export +- Breaking change: Moved out remaining modular i18n (imagelib) to own folder +- Breaking change: Drop `executeAfterLoads` (and getJSPDF/getCanvg) +- Breaking change: `RGBColor` must accept `new` +- Breaking change: canvg - `stackBlurCanvasRGBA` must be set now by function + (`setStackBlurCanvasRGBA`) rather than global; `canvg` now a named export +- Breaking change: Avoid passing `canvg`/`buildCanvgCallback` to extensions + (have them import) +- Breaking change: Have `readLang` return lang and data but do not call + `setLang` +- Breaking change: Avoid adding `assignAttributes`, `addSVGElementFromJson`, + `call`, `copyElem`, `findDefs`, `getElem`, `getId`, `getIntersectionList`, + `getMouseTarget`, `getNextId`, `getUrlFromAttr`, `hasMatrixTransform`, + `matrixMultiply`, `recalculateAllSelectedDimensions`, + `recalculateDimensions`, `remapElement`, `removeUnusedDefElems`, `round`, + `runExtensions`, `sanitizeSvg`, `setGradient` `transformListToTransform` + (and mistaken `toString` export) to `getPrivateMethods` (passed to + extensions) as available as public ones +- npm: Add `prepublishOnly` script to ensure building/testing before publish +- npm: Update devDeps including Rollup, Sinon +- Fix: Remove redundant (and incorrect) length set. (#256 ; fixes #255) +- Fix: Detection of whether to keep ellipse (rx and ry when just created + are now returning 0 instead of null); also with rectangle/square; + fixes #262 +- Fix: Avoid erring during resize on encountering MathML (which have no + `style`) +- Fix: Have general locales load first so extensions may use +- Fix: Provide `importLocale` to extensions `init` so it may delay + adding of the extension until locale data loaded +- Fix: i18nize imaglib more deeply +- Fix: Positioning of Document Properties dialog (Fixes #246) +- Fix (regression): PDF Export (Fixes #249) +- Fix (regression): Add polyfill for `ChildNode`/`ParentNode` (and use further) +- Fix (regression): Apply Babel universally to dependencies +- Fix (regression): Ordering of `uaPrefix` function in `svgEditor.js` +- Fix (regression): Embedded API +- Fix (embedded editor): Fix backspace key in Firefox so it doesn't navigate + out of frame +- Fix: Alert if no `exportWindow` for PDF (e.g., if blocked) +- Fix: Ensure call to `rasterExport` without `imgType` properly sets MIME + type to PNG +- Fix (extension): Wrong name for moinsave +- Fix (extension): ForeignObject editor +- Fix (Embedded API): Cross-domain may fail to even access `origin` or + `contentDocument` +- Fix (Embedded API): Avoid adding URL to iframe src if there are no arguments +- Fix (Embedded API): Handle origin issues (fixes #173) +- Fix (Cross-domain usage): Recover from exceptions with `localStorage` +- Fix regression (Imagelib): Fix path for non-module version +- Update: Update WebAppFind per new API changes +- Enhancement: Link to rawgit/raw.githack for live master demos (fixes #43) +- Enhancement: Make `setStrings` public on editor for late setting (used + by `ext-shapes.js`) +- Enhancement: Add `extensions_added` event +- Enhancement: Add `message` event (Relay messages including those which + have been been received prior to extension load) +- Enhancement: Sort SVG attributes alphabetically (#252 @Neil Fraser) +- Enhancement: Allow callback argument and return promise + for canvas methods: `rasterExport` and `exportPDF` +- Enhancement: Add `pointsAdded` canvas event (Fixes #141) +- Enhancement: Allow SVGEdit to work out of the box--avoid need for copying + sample config file. Should also help with Github-based file servers +- Enhancement: Allow avoiding "name" in extension export (just extract out + of file name) +- Enhancement: Add stack blur to canvg by default (and refactoring it) +- Enhancement: Return `Promise` for `embedImage` (as with some other loading + methods) +- Enhancement: Supply `importLocale` to `langReady` to facilitate extension + locale loading +- Enhancement: Recover if an extension fails to load (just log and otherwise + ignore) +- Enhancement: More i18n of extensions +- Enhancement: Allowing importing of locales within `addLangData` +- i18n: Clarify locale messages (where still available as English) to reflect + fact that Chrome only has "Save as" via context menu/right-click, not via + file menu (toward #192) +- Refactoring: Sort Embedded functions alphabetically and add lbs for better + visibility in code +- Refactoring: Simplify `isValidUnit` +- Refactoring( RGBColor) `RGBColor` as class, without rebuilding + constants, optimize string replacement, move methods to prototype, + use templates and object literals, use `Object.keys` +- Refactoring (canvg) Use classes more internally, use shorthand objects; + array extras, return to lazy-loading +- Refactoring: Use Promises in place of `$.getScript`; always return + Promises in case deciding to await resolving +- Refactoring: Avoid importing `RGBColor` into `svgutils.js` (jsPDF imports + it itself) +- Refactoring: Arrow functions, destructuring, shorter property references +- Refactoring: Fix `lang` and `dir` for locales (though not in use + currently anyways) +- Refactoring: Provide path config for canvg, jspdf +- Refactoring: Drop code for extension as function (already requiring export + to be an object) +- Refactoring: Object destructuring, `Object.entries`, Object shorthand, + array extras, more camelCase variable names +- Refactoring: Add a `Command` base class +- Refactoring: Simplify svgicons `callback` ready detection +- Refactoring: Put `let` or `const` closer to scope +- Refactoring: Remove unneeded `delimiter` from regex escaping utility +- Refactoring: Clearer variable names +- Refactoring: Use (non-deprecated) Event constructors +- Refactoring (minor): variadic args through ellipsis +- Refactoring (minor): `getIssues` to return codes and strings, lbs +- Refactoring (minor): Use single quotes in PHP +- Docs (Code comments): Coding standards within +- Docs: Transfer some changes from ExtensionDocs on wiki (need to fully + reconcile) +- Docs: Reference JSDocs in README +- Docs (ReleaseInstructions): Update +- Docs: Migrate copies of all old wiki pages to docs/from-old-wiki + folder; intended for a possible move to Markdown, so raw HTML + (with formatting) was not preserved, though named links were carried over + with absolute URLs +- Docs: Begin deleting `SvgCanvas.md` as ensuring jsdoc has replacements +- Docs: Add Edtior doc file for help to general users +- Docs: Clarify/simplify install instructions +- Docs: Generally update/improve docs (fixes #92) +- Docs: Update links to `latest` path (Avoid needing to update such + references upon each release) +- Docs: 80 chars max +- npm/Docs (JSDoc): Add script to check for overly generic types +- Docs (JSDoc): Move jsdoc output to public directory so may be visible + on releases (while still having in a `.gitignore`) +- Docs (JSDoc): Exclusions +- Docs (JSDoc): Add items; fix table layout +- Docs (JSDoc): For config/prefs and extension creating, link to tutorials + (moved tutorials to own directory to avoid recursion problems by jsdoc) +- Docs (JSDoc): Add modules (upper case for usual main entrance files or + regular names) +- Docs (JSDoc): Fill out missing areas; indicate return of `undefined`; + consistency with `@returns` +- Docs (JSDoc): Use Markdown plugin over HTML +- Docs (JSDoc): Add our own layout template to support overflow +- Docs (JSDoc): Use cleverLinks and disallow unknown tags +- Docs (JSDoc): Insist on "pedantic" flag; put output directory in config +- Docs (JSDoc): Use more precise Integer/Float over number, the specific type + of array/function/object +- Docs (JSDoc): Use `@throws`, `@enum`, `@event`/`@fires`/`@listens` +- Linting (ESLint): Avoid linting jsdoc folder +- Testing: Use new Sinon + +# 3.0.0-alpha.4 + +- Docs: Convert more docs to JSDoc and add JSDoc script (thanks, tetedacier!) +- Fix `main` on `package.json` to reference UMD distribution and `module` + to point to ES6 Module dist +- Fix (regression): Bad name on function passed to `path.js` +- Fix (regression): Star tool (radialshift) +- Fix (regression): Favicon setting + +# 3.0.0-alpha.3 + +- Change: Default to stylesheet above `editor` directory +- Docs: Installation steps +- Fix regression (Connector extension): Get config object when available +- Fix regression (Extensions): Use `extIconsPath` for now given + that `extPath` will not work relative to `dist` +- Fix regression: Enforce stylesheet ordering sequence +- Fix regression: Ensure SVG-edit hidden until stylesheets loaded +- Fix regression: Avoid abandoning stylesheet loading if one load fails +- Fix (ext-connector): Handle case of 2 connecting elements with + same y-coordinate (fixes #210 ; thanks, @iuyiuy!) +- Enhancement: Delete the imge upon cancel if it is a new image (fixes #177) +- Enhancement: Allow `addSvgElementFromJson` to accept non-SVG namespaces + with explicit `namespace` property as well as the default SVG namespace + (fixes #155); document +- Optimization: For `setSvgString`, if element content is not SVG, + return `false` earlier (Fixes #152); thanks iuyiuy! +- Demos: Add svgcanvas demo (Neil Fraser) +- npm: Update devDeps + +# 3.0.0-alpha.2 + +- Licensing: Indicate MIT is license type of rgbcolor and rename + file to reflect it; rename/add license file name for jgraduate + and screencast to reflect type (Apache 2.0) +contains license information (of type MIT) for Raphael icons +- Breaking change: Rename config file to `svgedit-config-iife.js` (or + for the module version, `svgedit-config-es.js`); also expect + one directory higher; incorporates #207 (@iuyiuy) +- Breaking change: Separate `extIconsPath` from `extPath` (not copying over icons) +- Breaking change: Don't reference `custom.css` in HTML; can instead + be referenced in JavaScript through the config file (provided in `svgedit-config-sample-iife.js`/`svgedit-config-sample-es.js` as + `svgedit-custom.css` for better namespacing); incorporates #207 (@iuyiuy) +- Breaking change: Remove minified jgraduate/spinbtn files (minified within Rollup routine) +- Breaking change: Require `new` with `EmbeddedSVGEdit` (allows us to use `class` internally) +- Breaking change: `svgcanvas.setUiStrings` must now be called if not using + editor in order to get strings (for sake of i18n) (and if using path.js + alone, must also have its `setUiStrings` called) +- Breaking change (ext-overview-window): Avoid global `overviewWindowGlobals` +- Breaking change (ext-imagelib): Change to object-based encoding for namespacing of +messages (though keep stringifying/parsing ourselves until we remove IE9 support) +- Breaking change: Rename `jquery.js` to `jquery.min.js` +- Breaking change: Remove `scoped` attribute from `style`; it is now deprecated and +obsolete; also move to head (after other stylesheets) +- Fix: i18nize path.js strings and canvas notifications +- Fix: Attempt i18n for ext-markers +- Fix: Zoom when scrolled; incorporates #169 (@AndrolGenhald), adapting for conventions; also allow avoidance when shift key pressed +- Fix: Update Atom feed reference in HTML +- Fix: Broken "lv" locale (and inconsistent tabs/spaces pt-PR) +- Fix: Inadvertent global assignments (uncovered by ESLint): + * `addBezierCurve` in `canvg.js` had undeclared `i` + * Fix: Undeclared variable in opera widget + * jgraduate->jpicker: Fix missing `var` for some `i` loops + * Fix: Globals (`x`, `y`) in `mouseMove` + * Fix: Global (`element`, `d_attr` (now renamed to `dAttr`)) in `mouseDown` + * Testing (math_test): Fix undeclared variables + * Screencast `showNotes` +- Fix: Bad scope closure references + * An apparent bug in `jquery.svgicons.js` whereby a variable + `holder` was declared in too nested of a scope + * Fix: Avoid `drawnPath` not defined error (currently adds as a global, but + should be switching to modules anyways) +- Fix (jgraduate->jpicker): Fix Color val check when `name.length` is empty + (equal to "all") +- Fix (jquery.jgraduate.js): Ensure `numstops` is present before check +- Fix (history.js) Relocation of rotational transform had undeclared variable (`elem`) +- Fix (Editor): Restore save alert +- Fix (Firefox svgutils.js): tspan (and textPath apparently) have no `getBBox` + in Firefox, so recover (fixes FF issue with recalculate test 3: + "recalculateDimensions() on text w/tspan with simple translate") +- Fix (Chrome recalculate.js): Chrome has a + [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=843901) + in not performing `removeAttribute` after `removeItem`; deal with it + (though only if there is a single identity matrix) (fixes Chrome issue + with recalculate test 1: + "recalculateDimensions() on rect with identity matrix") +- Fix (HTML): Update assorted links, including using `https://` +- Enhancement: ES6 modules (including jQuery plugins, extensions, locales, + tests), along with Babel; make Node build routine for converting modular + source to non-modular +- Enhancement: use `loadStylesheets` for modular stylesheet defining + (but parallel loading) +- Enhancement: Add `stylesheets` config for modular but parallel + stylesheet loading with `@default` option for simple + inclusion/exclusion of defaults (if not going with default). +- Enhancement: Further JSDoc (incomplete) +- Enhancement (Project size): Remove now unused Python l10n scripts (#238) +- Enhancement (Optimization): Compress images using imageoptim (and add +npm script) (per #215) +- Enhancement (Editor): Use `https` (instead of `http`) for link default +- Enhancement: Throw Error objects instead of strings (including in jgraduate->jpicker) +- Enhancement: Make SpinButton plugin independent of SVGEdit via + generic state object for `tool_scale` +- Enhancement: Move `config-sample.js` out of `editor` directory +- Enhancement: For `callback`-style extensions, also provide config + object; add following to that object: `buildCanvgCallback`, `canvg`, + `decode64`, `encode64`, `executeAfterLoads`, `getTypeMap`, `isChrome`, + `ieIE`, `NS`, `text2xml` +- npm: Add ESLint, uglify, start scripts +- npm: Update devDeps +- npm: Add html modules and config build to test script +- Docs: Remove "dependencies" comments in code except where summarizing + role of jQuery or a non-obvious dependency +- Linting: 2 spaces, remove BOM, remove carriage returns, bad characters + in Persian locale file +- Linting (ESLint): Numerous changes +- Refactoring: Switch to ESLint in source +- Refactoring: Move scripts to own files +- Refactoring: Clean up `svg-editor.html`: consistent indents; avoid extra lbs, avoid long lines +- Refactoring: Avoid embedded API adding inline JavaScript listener +- Refactoring: Move layers and context code to `draw.js` +- Refactoring: Move `pathActions` from `svgcanvas.js` (though preserve aliases to these methods on `canvas`) and `convertPath` from `svgutils.js` to `path.js` +- Refactoring: Move `getStrokedBBox` from `svgcanvas.js` (while keeping an alias) to `svgutils.js` (as `getStrokedBBoxDefaultVisible` to avoid conflict with existing) +- Refactoring/Linting: Enfore `no-extra-semi` and `quote-props` rules +- Refactoring: Further avoidance of quotes on properties (as possible) +- Refactoring: Use `class` in place of functions where intended as classes +- Refactoring: Consistency and granularity in extensions imports +- Refactoring (ext-storage): Move locale info to own file imported by the extension (toward modularity; still should be split into separate files by language and *dynamically* imported, but we'll wait for better `import` support to refactor this) +- Refactoring: For imagelib, add local jQuery copy (using old 1.4.4 as had +been using from server) +- Refactoring: For MathJax, add local copy (using old 2.3 as had been using from +server); server had not been working +- Refactoring: Remove `use strict` (implicit in modules) +- Refactoring: Remove trailing whitespace, fix some code within comments +- Refactoring: Expect `jQuery` global rather than `$` for better modularity +(also to adapt line later once available via `import`) +- Refactoring: Prefer `const` (and then `let`) +- Refactoring: Add block scope keywords closer to first block in which they appear +- Refactoring: Use ES6 `class` +- Refactoring `$.isArray` -> `Array.isArray` and avoid some other jQuery core methods +with simple VanillaJS replacements +- Refactoring: Use abbreviated object property syntax +- Refactoring: Object destructuring +- Refactoring: Remove `uiStrings` contents in svg-editor.js (obtains from locale) +- Refactoring: Add favicon to embedded API file +- Refactoring: Use arrow functions for brief functions (incomplete) +- Refactoring: Use `Array.prototype.includes`/`String.prototype.includes`; +`String.prototype.startsWith`, `String.prototype.trim` +- Refactoring: Remove now unnecessary svgutils do/while resetting of variables +- Refactoring: Use shorthand methods for object literals (avoid ": function") +- Refactoring: Avoid quoting object property keys where unnecessary +- Refactoring: Just do truthy/falsey check for lengths in place of comparison to 0 +- Refactoring (Testing): Avoid jQuery usage within most test files (defer script, +also in preparation for future switch to ES6 modules for tests) +- Refactoring: Make jpicker variable declaration indent bearable +- Refactoring (Linting): Finish svgcanvas.js +- Docs: Mention in comment no longer an entry file as before +- Docs: Migrate old config, extensions, and FAQ docs +- Build: Update minified version of spinbtn/jgraduate/jpicker per linted/improved files +- Testing: Move JavaScript out of HTML to own files +- Testing: Add `node-static` to get tests working +- Testing: Fix timing of `all_tests.html` for ensuring expanding iframe size to fit content +- Testing: Add favicon to test files (also may avoid extra log in console) +- Testing: Update QUnit to 2.6.1 (node_modules) and Sinon to 5.0.8 (and add sinon-test at 2.1.3) and enforce eslint-plugin-qunit linting rules; update custom extensions +- Testing: Add node-static for automating (and accessing out-of-directory contents) +- Testing: Avoid HTML attributes for styling +- Testing: Add npm `test` script +- Testing: Comment out unused jQuery SVG test +- Testing: Add test1 and svgutils_performance_test to all tests page +- Testing: Due apparently to Path having not been a formal class, the test was calling it without `new`; refactored now with sufficient mock data to take into account it is a class + +# 3.0.0-alpha.1 + +(Only released on npm) + +- Provide `package.json` for npm to reserve name (reflecting current state of `master`) + # 2.8.1 (Ellipse) - December 2nd, 2015 For a complete list of changes run: @@ -6,11 +400,11 @@ For a complete list of changes run: git log 81afaa9..5986f1e ``` -* Enhancement: Use `getIntersectionList` when available () -* Enhancement: Switched to https for all URLs () -* Enhancement: Minor administrative updates (docs/, README.md, author emails) -* Fix: Bug where all icons were broken in Safari () -* Fix: Updated translations for "page" and "delete" in 57 locales. +- Enhancement: Use `getIntersectionList` when available () +- Enhancement: Switched to https for all URLs () +- Enhancement: Minor administrative updates (docs/, README.md, author emails) +- Fix: Bug where all icons were broken in Safari () +- Fix: Updated translations for "page" and "delete" in 57 locales. # 2.8 (Ellipse) - November 24th, 2015 @@ -20,30 +414,30 @@ For a complete list of changes run: git log 4bb15e0..253b4bf ``` -* Enhancement (Experimental): Client-side PDF export (issue [#1156](https://code.google.com/p/svg-edit/issues/detail?id=1156)) (to data: URI) and server-side PDF export (where not supported in browser and using ext-server_opensave.js); uses [jsPDF](https://github.com/MrRio/jsPDF) library -* Enhancement: For image exports, provided "datauri" property to "exported" event. -* Enhancement: Allow config "exportWindowType" of value "new" or "same" to indicate whether to reuse the same export window upon subsequent exports -* Enhancement: Added openclipart support to imagelib extension -* Enhancement: allow showGrid to be set before load -* Enhancement: Support loading of (properly URL encoded) non-base64 "data:image/svg+xml;utf8,"-style data URIs -* Enhancement: More clear naming of labels: "Open Image"->"Open SVG" and "Import SVG"->"Import Image" ( issue [#1206](https://code.google.com/p/svg-edit/issues/detail?id=1206)) -* Enhancement: Included reference to (repository-ignored) custom.css file which once created by the user, as with config.js, allows customization without modifying the repo (its main editor file) -* Enhancement: Updated Slovenian locale. -* Demo enhancement: Support and demonstrate export in embedded editor -* Upgrade: canvg version -* Upgrade: Added PathSeg polyfill to workaround pathseg removal in browsers. -* Fix: pathtool bug where paths were erroneously deleted. -* Fix: Context menu did not work for groups. -* Fix: Avoid error in ungrouping function when no elements selected (was impacting MathJax "Ok" button). -* Fix: issue [#1205](https://code.google.com/p/svg-edit/issues/detail?id=1205) with Snap to Grid preventing editing -* Fix: bug in exportImage if svgEditor.setCustomHandlers calls made -* Fix: Ensure "loading..." message closes upon completion or error -* Fix: Ensure all dependencies are first available before canvg (and jsPDF) usage -* Fix: Allow for empty images -* Fix: Minor improvement in display when icon size is set to small -* Fix: Based64-encoding issues with Unicode text (e.g., in data URIs or icons) -* Fix: 2.7 regression in filesave.php for SVG saving (used by ext-server_opensave.js when client doesn't support the download attribute) -* Potentially breaking API changes (subject to further alteration before release): +- Enhancement (Experimental): Client-side PDF export (issue [#1156](https://code.google.com/p/svg-edit/issues/detail?id=1156)) (to data: URI) and server-side PDF export (where not supported in browser and using ext-server_opensave.js); uses [jsPDF](https://github.com/MrRio/jsPDF) library +- Enhancement: For image exports, provided "datauri" property to "exported" event. +- Enhancement: Allow config "exportWindowType" of value "new" or "same" to indicate whether to reuse the same export window upon subsequent exports +- Enhancement: Added openclipart support to imagelib extension +- Enhancement: allow showGrid to be set before load +- Enhancement: Support loading of (properly URL encoded) non-base64 "data:image/svg+xml;utf8,"-style data URIs +- Enhancement: More clear naming of labels: "Open Image"->"Open SVG" and "Import SVG"->"Import Image" ( issue [#1206](https://code.google.com/p/svg-edit/issues/detail?id=1206)) +- Enhancement: Included reference to (repository-ignored) custom.css file which once created by the user, as with config.js, allows customization without modifying the repo (its main editor file) +- Enhancement: Updated Slovenian locale. +- Demo enhancement: Support and demonstrate export in embedded editor +- Upgrade: canvg version +- Upgrade: Added PathSeg polyfill to workaround pathseg removal in browsers. +- Fix: pathtool bug where paths were erroneously deleted. +- Fix: Context menu did not work for groups. +- Fix: Avoid error in ungrouping function when no elements selected (was impacting MathJax "Ok" button). +- Fix: issue [#1205](https://code.google.com/p/svg-edit/issues/detail?id=1205) with Snap to Grid preventing editing +- Fix: bug in exportImage if svgEditor.setCustomHandlers calls made +- Fix: Ensure "loading..." message closes upon completion or error +- Fix: Ensure all dependencies are first available before canvg (and jsPDF) usage +- Fix: Allow for empty images +- Fix: Minor improvement in display when icon size is set to small +- Fix: Based64-encoding issues with Unicode text (e.g., in data URIs or icons) +- Fix: 2.7 regression in filesave.php for SVG saving (used by ext-server_opensave.js when client doesn't support the download attribute) +- Potentially breaking API changes (subject to further alteration before release): * Remove 2.7-deprecated "pngsave" (in favor of "exportImage") * Data URIs must be properly URL encoded (use encodeURIComponent() on the "data:..." prefix and double encodeURIComponent() the remaining content) * Remove "paramurl" parameter (use "url" or "source" with a data: URI instead) @@ -53,147 +447,147 @@ git log 4bb15e0..253b4bf # 2.7.1 (applied to 2.7 branch) - April 17, 2014 -* Fix important ID situation with embedded API -* Update functions available to embedded editor +- Fix important ID situation with embedded API +- Update functions available to embedded editor # 2.7 (Deltoid curve) - April 7th, 2014 -* Export to PNG, JPEG, BMP, WEBP (including quality control for JPEG/WEBP) for default editor and for the server_opensave extension -* Added Star, Polygon, and Panning Extensions r2318 r2319 r2333 -* Added non-default extension, ext-xdomain-messaging.js, moving cross-domain messaging code (as used by the embedded editor) out of core and requiring, when the extension IS included, that configuration (an array "allowedOrigins") be set in order to allow access by any domain (even same domain). -* Cause embedded editor to pass on URL arguments to the child editor (child iframe) -* Added default extension, ext-storage.js moving storage setting code into this (optional) extension; contains dialog to ask user whether they wish to utilize local storage for prefs and/or content; provides configuration options to tweak behaviors. -* Allow for a new file config.js within the editor folder (but not committed to SVN and ignored) which is always loaded and can be used for supplying configuration which happens early enough to affect URL or user storage configuration, in addition to extension behavior configuration. Provided config-sample.js to indicate types of configuration one could use (see also defaultPrefs, defaultExtensions, and defaultConfig within svg-editor.js ) -* Added configuration "preventAllURLConfig", "lockExtensions", and/or "preventURLContentLoading" for greater control of what can be configured via URL. -* Allow second argument object to setConfig containing "allowInitialUserOverride" booleans to allow for preference config in config.js to be overridden by URL or preferences in user storage; also can supply "overwrite" boolean in 2nd argument object if set to false to prevent overwriting of any prior-set configuration (URL config/pref setting occurs in this manner automatically for security reasons). -* Allow server_opensave extension to work wholly client-side (if browser supports the download attribute) -* Added WebAppFind extension -* Added new php_savefile extension to replace outdated, non-functioning server-save code; requires user to create "savefile_config.php" file and do any validation there (for their own security) -* Use addEventListener for 'beforeunload' event so user can add their own if desired -* Changed locale behavior to always load from locale file, including English. Allow extensions to add new "langReady" callback which is passed an object with "lang" and "uiStrings" properties whenever the locale data is first made available or changed by the user (this callback will not be invoked until the locale data is available). Extensions can add strings to all locales and utilize this mechanism. -* Made fixes impacting path issues and also ext-connector.js -* Fixed a bug where the position number supplied on an extension object was too high (e.g., if too few other extensions were included, the extension might not show up because its position was set too high). -* Added Polish locale -* Zoom features -* Make extension paths relative within extensions (issue 1184) -* Security improvements and other fixes -* Embedded editor can now work same domain without JSON parsing and the consequent potential loss of arguments or return values. -* Potentially breaking API changes: -** Disallowed "extPath", "imgPath", "langPath", and "jGraduatePath" setting via URL and prevent cross-domain/cross-folder extensions being set by URL (security enhancement) -** Deprecated "pngsave" option called by setCustomHandlers() in favor of "exportImage" (to accommodate export of other image types). Second argument will now supply, in addition to "issues" and "svg", the properties "type" (currently 'PNG', 'JPEG', 'BMP', 'WEBP'), "mimeType", and "quality" (for 'JPEG' and 'WEBP' types). -** Default extensions will now always load (along with those supplied in the URL unless the latter is prohibited by configuration), so if you do not wish your old code to load all of the default extensions, you will need to add &noDefaultExtensions=true to the URL (or add equivalent configuration in config.js). ext-overview_window.js can now be excluded though it is still a default. -** Preferences and configuration options must be within the list supplied within svg-editor.js (should include those of all documented extensions). -** Embedded messaging will no longer work by default for privacy/data integrity reasons. One must include the "ext-xdomain-messaging.js" extension and supply an array configuration item, "allowedOrigins" with potential values including: "\*" (to allow all domains--strongly discouraged!), "null" as a string to allow file:// access, window.location.origin (to allow same domain access), or specific trusted origins. The embedded editor works without the extension if the main editor is on the same domain, but if cross-domain control is needed, the "allowedOrigins" array must be supplied by a call to svgEditor.setConfig({allowedOrigins: [origin1, origin2, etc.]}) in the new config.js file. +- Export to PNG, JPEG, BMP, WEBP (including quality control for JPEG/WEBP) for default editor and for the server_opensave extension +- Added Star, Polygon, and Panning Extensions r2318 r2319 r2333 +- Added non-default extension, ext-xdomain-messaging.js, moving cross-domain messaging code (as used by the embedded editor) out of core and requiring, when the extension IS included, that configuration (an array "allowedOrigins") be set in order to allow access by any domain (even same domain). +- Cause embedded editor to pass on URL arguments to the child editor (child iframe) +- Added default extension, ext-storage.js moving storage setting code into this (optional) extension; contains dialog to ask user whether they wish to utilize local storage for prefs and/or content; provides configuration options to tweak behaviors. +- Allow for a new file config.js within the editor folder (but not committed to SVN and ignored) which is always loaded and can be used for supplying configuration which happens early enough to affect URL or user storage configuration, in addition to extension behavior configuration. Provided config-sample.js to indicate types of configuration one could use (see also defaultPrefs, defaultExtensions, and defaultConfig within svg-editor.js ) +- Added configuration "preventAllURLConfig", "lockExtensions", and/or "preventURLContentLoading" for greater control of what can be configured via URL. +- Allow second argument object to setConfig containing "allowInitialUserOverride" booleans to allow for preference config in config.js to be overridden by URL or preferences in user storage; also can supply "overwrite" boolean in 2nd argument object if set to false to prevent overwriting of any prior-set configuration (URL config/pref setting occurs in this manner automatically for security reasons). +- Allow server_opensave extension to work wholly client-side (if browser supports the download attribute) +- Added WebAppFind extension +- Added new php_savefile extension to replace outdated, non-functioning server-save code; requires user to create "savefile_config.php" file and do any validation there (for their own security) +- Use addEventListener for 'beforeunload' event so user can add their own if desired +- Changed locale behavior to always load from locale file, including English. Allow extensions to add new "langReady" callback which is passed an object with "lang" and "uiStrings" properties whenever the locale data is first made available or changed by the user (this callback will not be invoked until the locale data is available). Extensions can add strings to all locales and utilize this mechanism. +- Made fixes impacting path issues and also ext-connector.js +- Fixed a bug where the position number supplied on an extension object was too high (e.g., if too few other extensions were included, the extension might not show up because its position was set too high). +- Added Polish locale +- Zoom features +- Make extension paths relative within extensions (issue 1184) +- Security improvements and other fixes +- Embedded editor can now work same domain without JSON parsing and the consequent potential loss of arguments or return values. +- Potentially breaking API changes: + * Disallowed "extPath", "imgPath", "langPath", and "jGraduatePath" setting via URL and prevent cross-domain/cross-folder extensions being set by URL (security enhancement) + * Deprecated "pngsave" option called by setCustomHandlers() in favor of "exportImage" (to accommodate export of other image types). Second argument will now supply, in addition to "issues" and "svg", the properties "type" (currently 'PNG', 'JPEG', 'BMP', 'WEBP'), "mimeType", and "quality" (for 'JPEG' and 'WEBP' types). + * Default extensions will now always load (along with those supplied in the URL unless the latter is prohibited by configuration), so if you do not wish your old code to load all of the default extensions, you will need to add `&noDefaultExtensions=true` to the URL (or add equivalent configuration in config.js). ext-overview_window.js can now be excluded though it is still a default. + * Preferences and configuration options must be within the list supplied within svg-editor.js (should include those of all documented extensions). + * Embedded messaging will no longer work by default for privacy/data integrity reasons. One must include the "ext-xdomain-messaging.js" extension and supply an array configuration item, "allowedOrigins" with potential values including: "\*" (to allow all domains--strongly discouraged!), "null" as a string to allow file:// access, window.location.origin (to allow same domain access), or specific trusted origins. The embedded editor works without the extension if the main editor is on the same domain, but if cross-domain control is needed, the "allowedOrigins" array must be supplied by a call to svgEditor.setConfig({allowedOrigins: [origin1, origin2, etc.]}) in the new config.js file. # 2.6 (Cycloid) - January 15th, 2013 -* Support for Internet Explorer 9 -* Context menu -* Cut/Copy/Paste/Paste in Place options -* Gridlines, snap to grid -* Merge layers -* Duplicate layer -* Image library -* Shape library -* Basic Server-based tools for file opening/saving -* In-group editing -* Cut/Copy/Paste -* full list: http://code.google.com/p/svg-edit/issues/list?can=1&q=label%3ANeededFor-2.6 +- Support for Internet Explorer 9 +- Context menu +- Cut/Copy/Paste/Paste in Place options +- Gridlines, snap to grid +- Merge layers +- Duplicate layer +- Image library +- Shape library +- Basic Server-based tools for file opening/saving +- In-group editing +- Cut/Copy/Paste +- full list: http://code.google.com/p/svg-edit/issues/list?can=1&q=label%3ANeededFor-2.6 # 2.5 - June 15, 2010 -* Open Local Files (Firefox 3.6+ only) -* Import SVG into Drawing (Firefox 3.6+ only) -* Ability to create extensions/plugins -* Main menu and overal interface improvements -* Create and select elements outside the canvas -* Base support for the svg:use element -* Add/Edit Sub-paths -* Multiple path segment selection -* Radial Gradient support -* Connector lines -* Arrows & Markers -* Smoother freehand paths -* Foreign markup support (ForeignObject?/MathML) -* Configurable options -* File-loading options -* Eye-dropper tool (copy element style) -* Stroke linejoin and linecap options -* Export to PNG -* Blur tool -* Page-align single elements -* Inline text editing -* Line draw snapping with Shift key +- Open Local Files (Firefox 3.6+ only) +- Import SVG into Drawing (Firefox 3.6+ only) +- Ability to create extensions/plugins +- Main menu and overall interface improvements +- Create and select elements outside the canvas +- Base support for the svg:use element +- Add/Edit Sub-paths +- Multiple path segment selection +- Radial Gradient support +- Connector lines +- Arrows & Markers +- Smoother freehand paths +- Foreign markup support (ForeignObject?/MathML) +- Configurable options +- File-loading options +- Eye-dropper tool (copy element style) +- Stroke linejoin and linecap options +- Export to PNG +- Blur tool +- Page-align single elements +- Inline text editing +- Line draw snapping with Shift key # 2.4 - January 11, 2010 -* Zoom -* Layers -* UI Localization -* Wireframe Mode -* Resizable UI (SVG icons) -* Set background color and/or image (for tracing) -* Convert Shapes to Paths -* X, Y coordinates for all elements -* Draggable Dialog boxes -* Select Non-Adjacent Elements -* Fixed-ratio resize -* Automatic Tool Switching -* Raster Images -* Group elements -* Add/Remove path nodes -* Curved Paths -* Floating point values for all attributes -* Text fields for all attributes -* Title element +- Zoom +- Layers +- UI Localization +- Wireframe Mode +- Resizable UI (SVG icons) +- Set background color and/or image (for tracing) +- Convert Shapes to Paths +- X, Y coordinates for all elements +- Draggable Dialog boxes +- Select Non-Adjacent Elements +- Fixed-ratio resize +- Automatic Tool Switching +- Raster Images +- Group elements +- Add/Remove path nodes +- Curved Paths +- Floating point values for all attributes +- Text fields for all attributes +- Title element # 2.3 - September 08, 2009 -* Align Objects -* Rotate Objects -* Clone Objects -* Select Next/Prev Object -* Edit SVG Source -* Gradient picking -* Polygon Mode (Path Editing, Phase 1) +- Align Objects +- Rotate Objects +- Clone Objects +- Select Next/Prev Object +- Edit SVG Source +- Gradient picking +- Polygon Mode (Path Editing, Phase 1) # 2.2 - July 08, 2009 -* Multiselect Mode -* Undo/Redo Actions -* Resize Elements -* Contextual tools for rect, circle, ellipse, line, text elements -* Some updated button images -* Stretched the UI to fit the browser window -* Resizing of the SVG canvas -* Upgraded to jPicker 1.0.8 +- Multiselect Mode +- Undo/Redo Actions +- Resize Elements +- Contextual tools for rect, circle, ellipse, line, text elements +- Some updated button images +- Stretched the UI to fit the browser window +- Resizing of the SVG canvas +- Upgraded to jPicker 1.0.8 # 2.1 - June 17, 2009 -* tooltips added to all UI elements -* fix flyout menus -* ask before clearing the drawing (suggested by martin.vidner) -* control group, fill and stroke opacity -* fix flyouts when using color picker -* change license from GPLv2 to Apache License v2.0 -* replaced Farbtastic with jPicker, because of the license issues -* removed dependency on svgcanvas.svg, now created in JavaScript -* added Select tool -* using jQuery hosted by Google instead of local version -* allow dragging of elements -* save SVG file to separate tab -* create and edit text elements -* context panel tools -* change rect radius, font-family, font-size -* added keystroke shortcuts for all tools -* move to top/bottom +- tooltips added to all UI elements +- fix flyout menus +- ask before clearing the drawing (suggested by martin.vidner) +- control group, fill and stroke opacity +- fix flyouts when using color picker +- change license from GPLv2 to Apache License v2.0 +- replaced Farbtastic with jPicker, because of the license issues +- removed dependency on svgcanvas.svg, now created in JavaScript +- added Select tool +- using jQuery hosted by Google instead of local version +- allow dragging of elements +- save SVG file to separate tab +- create and edit text elements +- context panel tools +- change rect radius, font-family, font-size +- added keystroke shortcuts for all tools +- move to top/bottom # 2.0 - June 03, 2009 -* rewritten SVG-edit, so now it uses OOP -* draw ellipse, square -* created HTML interface similar to Inkscape +- rewritten SVG-edit, so now it uses OOP +- draw ellipse, square +- created HTML interface similar to Inkscape # 1.0 - February 06, 2009 -* SVG-Edit released +- SVG-Edit released diff --git a/LICENSE b/LICENSE-MIT.txt similarity index 94% rename from LICENSE rename to LICENSE-MIT.txt index d075ca6e..4f78db60 100644 --- a/LICENSE +++ b/LICENSE-MIT.txt @@ -1,4 +1,4 @@ -Copyright (c) 2009-2012 by SVG-edit authors (see AUTHORS file) +Copyright (c) 2009-2018 by SVG-edit authors (see AUTHORS file) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index ad18f3f0..3a50532d 100644 --- a/Makefile +++ b/Makefile @@ -8,15 +8,15 @@ ZIP=zip # All files that will be compiled by the Closure compiler. JS_FILES=\ - svgedit.js \ - jquery-svg.js \ - contextmenu/jquery.contextMenu.js \ - pathseg.js \ + namespaces.js \ + jQuery.attr.js \ + contextmenu/jQuery.contextMenu.js \ + svgpathseg.js \ browser.js \ svgtransformlist.js \ math.js \ units.js \ - svgutils.js \ + utilities.js \ sanitize.js \ history.js \ historyrecording.js \ @@ -93,7 +93,7 @@ chrome: cd build ; $(ZIP) -r $(PACKAGE)-crx.zip svgedit_app ; rm -rf svgedit_app; cd .. jgraduate: - java -jar $(CLOSURE) --js editor/jgraduate/jquery.jgraduate.js --js_output_file editor/jgraduate/jquery.jgraduate.min.js + java -jar $(CLOSURE) --js editor/jgraduate/jquery.jGraduate.js --js_output_file editor/jgraduate/jquery.jgraduate.min.js clean: rm -rf config rm -rf build/$(PACKAGE) diff --git a/README.md b/README.md index 19ff1d97..e7b9a299 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,121 @@ -![alt text](https://svg-edit.github.io/svgedit/images/logo48x48.svg "svg-edit logo of a pencil") SVG-edit -=== -SVG-edit is a fast, web-based, javascript-driven SVG drawing editor that works in any modern browser. +# ![alt text](https://svg-edit.github.io/svgedit/images/logo48x48.svg "svg-edit logo of a pencil") SVG-edit -### [Try SVG-edit here](https://svg-edit.github.io/svgedit/releases/svg-edit-2.8.1/svg-editor.html) +SVG-edit is a fast, web-based, JavaScript-driven SVG drawing editor that +works in any modern browser. -(Also available as a [download](https://github.com/SVG-Edit/svgedit/releases/download/svg-edit-2.8.1/svg-edit-2.8.1.zip) in [releases](https://github.com/SVG-Edit/svgedit/releases)). +## Demo + +### [Try SVG-edit here](https://svg-edit.github.io/svgedit/releases/latest/editor/svg-editor.html) + +See the [latest release](https://svg-edit.github.io/svgedit/releases/latest/editor/svg-editor.html) +(or its [ES6-Module](https://svg-edit.github.io/svgedit/releases/latest/editor/svg-editor.html) version, which requires a modern browser). + +Also available as a download in [releases](https://github.com/SVG-Edit/svgedit/releases). + +For testing the latest version in `master`, you may use or +. + +## Installation + +### Quick install + +1. Clone or copy the repository contents (at least the `editor` directory). +1. If you need programmatic customization, see its section below. +1. Otherwise, just add an iframe to your site, adding any extensions or + configuration (see `docs/tutorials/ConfigOptions.md` + ([ConfigOptions]{@tutorial ConfigOptions})) within the URL: +```html + +``` + +### Integrating SVG-edit into your own npm package + +These steps are only needed if you wish to set up your own npm package +incorporating SVGEdit. + +1. Create your npm package: `npm init` (complete the fields). +1. Install SVG-edit into your package: + `npm i svgedit`. +1. Look within `node_modules/svgedit/`, e.g., `node_modules/svgedit/editor/svg-editor.html` + for the files your package needs and use accordingly. +1. `npm publish` + +## Programmatic customization + +1. If you are not concerned about supporting ES6 Modules (see the + "ES6 Modules file" section), you can add your config directly to + `svgedit-config-iife.js` within the SVG-Edit project root. + 1. Note: Do not remove the `import svgEditor...` code which is responsible for + importing the SVG edit code. Versions prior to 3.0 did not require this, + but the advantage is that your HTML does not need to be polluted with + extra script references. +1. Modify or utilize any options. See `docs/tutorials/ConfigOptions.md` + ([ConfigOptions]{@tutorial ConfigOptions}). + +## ES6 Modules file + +1. `svg-editor-es.html` is an HTML file directly using ES6 modules. + It is only supported in the latest browsers. It is probably mostly + useful for debugging, as it requires more network requests. + If you would like to work with this file, you should make configuration + changes in `svgedit-config-es.js` (in the SVG-Edit project root). +1. If you are working with the ES6 Modules config but also wish to work with + the normal `svg-editor.html` version (so your code can work in older + browsers or get the presumable performance benefits of this file which + references JavaScript rolled up into a single file), you can follow these + steps after any config changes you make, so that your changes can also be + automatically made available to both versions. + 1. JavaScript: + 1. Run `npm install` within the `node_modules/svgedit` directory to + install the build tools for SVG-edit. + 1. Run `npm run build-config` within the `node_modules/svgedit` directory. + 1. This will rebuild `svgedit-config-iife.js` (applying Babel to allow + it to work on older browsers and applying Rollup to build all + JavaScript into one file). The file will then contain non-ES6 module + JavaScript that can work in older browsers. Note that it bundles all + of SVGEdit, so it is to be expected that this file will be much larger + in size than the original ES6 config file. + 1. HTML: + 1. If you wish to make changes to both HTML files, it is recommended that you + work and test on `svg-editor-es.html` and then run `npm run build-html` + to have the changes properly copied to `svg-editor.html`. ## Recent news - * 2017-07 Added to Packagist: https://packagist.org/packages/svg-edit/svgedit - * 2015-12-02 SVG-edit 2.8.1 was released. - * 2015-11-24 SVG-edit 2.8 was released. - * 2015-11-24 Code, issue tracking, and docs are being moved to github (previously [code.google.com](https://code.google.com/p/svg-edit)). - * 2014-04-17 2.7 and stable branches updated to reflect 2.7.1 important bug fixes for the embedded editor. - * 2014-04-07 SVG-edit 2.7 was released. - * 2013-01-15 SVG-edit 2.6 was released. + +- 2018-07-31 Published 3.0.0-rc.2 with misc. fixes +- 2018-07-19 Published 3.0.0-rc.1 allowing for extensions and locales to be + expressed as modules +- 2018-05-26 Published 3.0.0-alpha.2 with ES6 Modules support +- 2017-07 Added to Packagist: https://packagist.org/packages/svg-edit/svgedit +- 2015-12-02 SVG-edit 2.8.1 was released. +- 2015-11-24 SVG-edit 2.8 was released. +- 2015-11-24 Code, issue tracking, and docs are being moved to github (previously [code.google.com](https://code.google.com/p/svg-edit)). +- 2014-04-17 2.7 and stable branches updated to reflect 2.7.1 important bug fixes for the embedded editor. +- 2014-04-07 SVG-edit 2.7 was released. +- 2013-01-15 SVG-edit 2.6 was released. ## Videos - * [SVG-edit 2.4 Part 1](http://www.youtube.com/watch?v=zpC7b1ZJvvM) - * [SVG-edit 2.4 Part 2](http://www.youtube.com/watch?v=mDzZEoGUDe8) - * [SVG-edit 2.3 Features](http://www.youtube.com/watch?v=RVIcIy5fXOc) - * [Introduction to SVG-edit](http://www.youtube.com/watch?v=ZJKmEI06YiY) (Version 2.2) + * [SVG-edit 2.4 Part 1](https://www.youtube.com/watch?v=zpC7b1ZJvvM) + * [SVG-edit 2.4 Part 2](https://www.youtube.com/watch?v=mDzZEoGUDe8) + * [SVG-edit 2.3 Features](https://www.youtube.com/watch?v=RVIcIy5fXOc) + * [Introduction to SVG-edit](https://www.youtube.com/watch?v=ZJKmEI06YiY) (Version 2.2) ## Supported browsers -The following browsers had been tested for 2.6 or earlier and will probably continue to work with 2.8. - * Firefox 1.5+ - * Opera 9.50+ - * Safari 4+ - * Chrome 1+ - * IE 9+ and Edge +The following browsers had been tested for 2.6 or earlier and will probably continue to work with 3.0. + +- Firefox 1.5+ +- Opera 9.50+ +- Safari 4+ +- Chrome 1+ +- IE 9+ and Edge ## Further reading and more information - * See [docs](docs/) for more documentation. + * See [docs](docs/) for more documentation. See the [JSDocs for our latest release](https://svg-edit.github.io/svgedit/releases/latest/docs/jsdoc/index.html). * [Acknowledgements](docs/Acknowledgements.md) lists open source projects used in svg-edit. * See [AUTHORS](AUTHORS) file for authors. - * [Stackoverflow](http://stackoverflow.com/tags/svg-edit) group. + * [StackOverflow](https://stackoverflow.com/tags/svg-edit) group. * Join the [svg-edit mailing list](https://groups.google.com/forum/#!forum/svg-edit). - * Join us on `#svg-edit` on `freenode.net` (or use the [web client](http://webchat.freenode.net/?channels=svg-edit)). + * Join us on `#svg-edit` on `freenode.net` (or use the [web client](https://webchat.freenode.net/?channels=svg-edit)). diff --git a/build-html.js b/build-html.js new file mode 100644 index 00000000..4af6825c --- /dev/null +++ b/build-html.js @@ -0,0 +1,101 @@ +/* eslint-env node */ +const fs = require('promise-fs'); + +const filesAndReplacements = [ + { + input: 'editor/svg-editor-es.html', + output: 'editor/xdomain-svg-editor-es.html', + replacements: [ + [ + '', + `` + ] + ] + }, + { + input: 'editor/xdomain-svg-editor-es.html', + output: 'editor/xdomain-svg-editor.html', + replacements: [ + [ + '', + ` +` + ], + [ + '', + '' + ], + [ + '', + '' + ], + [ + '', + '' + ] + ] + }, + { + input: 'editor/svg-editor-es.html', + output: 'editor/svg-editor.html', + replacements: [ + [ + '', + ` +` + ], + [ + '', + '' + ], + [ + '', + '' + ], + [ + '', + '' + ] + ] + }, + { + input: 'editor/extensions/imagelib/index-es.html', + output: 'editor/extensions/imagelib/index.html', + replacements: [ + [ + '', + ` +` + ], + [ + '', + '' + ] + ] + } +]; + +filesAndReplacements.reduce((p, {input, output, replacements}) => { + return p.then(async () => { + let data; + try { + data = await fs.readFile(input, 'utf8'); + } catch (err) { + console.log(`Error reading ${input} file`, err); + } + + data = replacements.reduce((s, [find, replacement]) => { + return s.replace(find, replacement); + }, data); + + try { + await fs.writeFile(output, data); + } catch (err) { + console.log(`Error writing file: ${err}`, err); + return; + } + console.log(`Completed file ${input} rewriting!`); + }); +}, Promise.resolve()).then(() => { + console.log('Finished!'); +}); diff --git a/build/tools/ship.py b/build/tools/ship.py index d2c30521..2cb5df6a 100755 --- a/build/tools/ship.py +++ b/build/tools/ship.py @@ -121,7 +121,6 @@ def parseComment(line, line_num, enabled_flags): return line - def ship(inFileName, enabled_flags): # read in HTML file lines = file(inFileName, 'r').readlines() @@ -141,7 +140,7 @@ def ship(inFileName, enabled_flags): else: # else append line to the output list out_lines.append(line) i += 1 - + return ''.join(out_lines) if __name__ == '__main__': diff --git a/chrome-app/icon_128.png b/chrome-app/icon_128.png index 964027fe..6369b49b 100644 Binary files a/chrome-app/icon_128.png and b/chrome-app/icon_128.png differ diff --git a/composer.json b/composer.json index 66779d24..a4da44ed 100644 --- a/composer.json +++ b/composer.json @@ -3,24 +3,28 @@ "description": "SVG-edit is a fast, web-based, javascript-driven SVG drawing editor that works in any modern browser.", "authors": [ { - "name":"Narendra Sisodiya", + "name": "Narendra Sisodiya", "email": "narendra@narendrasisodiya.com" }, { - "name":"Pavol Rusnak", + "name": "Pavol Rusnak", "email": "stick@gk2.sk" }, { - "name":"Jeff Schiller", + "name": "Jeff Schiller", "email": "codedread@gmail.com" }, { - "name":"Vidar Hokstad", + "name": "Vidar Hokstad", "email": "vidar.hokstad@gmail.com" }, { - "name":"Alexis Deveria", + "name": "Alexis Deveria", "email": "adeveria@gmail.com" + }, + { + "name": "Brett Zamir", + "email": "brettz9@yahoo.com" } ], "keywords": [ diff --git a/demos/canvas.html b/demos/canvas.html new file mode 100644 index 00000000..d5d9ee3d --- /dev/null +++ b/demos/canvas.html @@ -0,0 +1,54 @@ + + + + + Minimal demo of SvgCanvas + + + + + + +

Minimal demo of SvgCanvas

+ +
+ +
+ [ + + ] + + + + +
+ + + + diff --git a/dist/canvg.js b/dist/canvg.js new file mode 100644 index 00000000..40488aa3 --- /dev/null +++ b/dist/canvg.js @@ -0,0 +1,4405 @@ +var canvg = (function (exports) { + 'use strict'; + + var asyncToGenerator = function (fn) { + return function () { + var gen = fn.apply(this, arguments); + return new Promise(function (resolve, reject) { + function step(key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + return Promise.resolve(value).then(function (value) { + step("next", value); + }, function (err) { + step("throw", err); + }); + } + } + + return step("next"); + }); + }; + }; + + var classCallCheck = function (instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + }; + + var createClass = function () { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + return function (Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; + }(); + + var get = function get(object, property, receiver) { + if (object === null) object = Function.prototype; + var desc = Object.getOwnPropertyDescriptor(object, property); + + if (desc === undefined) { + var parent = Object.getPrototypeOf(object); + + if (parent === null) { + return undefined; + } else { + return get(parent, property, receiver); + } + } else if ("value" in desc) { + return desc.value; + } else { + var getter = desc.get; + + if (getter === undefined) { + return undefined; + } + + return getter.call(receiver); + } + }; + + var inherits = function (subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + enumerable: false, + writable: true, + configurable: true + } + }); + if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; + }; + + var possibleConstructorReturn = function (self, call) { + if (!self) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return call && (typeof call === "object" || typeof call === "function") ? call : self; + }; + + var slicedToArray = function () { + function sliceIterator(arr, i) { + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"]) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; + } + + return function (arr, i) { + if (Array.isArray(arr)) { + return arr; + } else if (Symbol.iterator in Object(arr)) { + return sliceIterator(arr, i); + } else { + throw new TypeError("Invalid attempt to destructure non-iterable instance"); + } + }; + }(); + + var toConsumableArray = function (arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; + + return arr2; + } else { + return Array.from(arr); + } + }; + + /** + * For parsing color values + * @module RGBColor + * @author Stoyan Stefanov + * @see https://www.phpied.com/rgb-color-parser-in-javascript/ + * @license MIT + */ + var simpleColors = { + aliceblue: 'f0f8ff', + antiquewhite: 'faebd7', + aqua: '00ffff', + aquamarine: '7fffd4', + azure: 'f0ffff', + beige: 'f5f5dc', + bisque: 'ffe4c4', + black: '000000', + blanchedalmond: 'ffebcd', + blue: '0000ff', + blueviolet: '8a2be2', + brown: 'a52a2a', + burlywood: 'deb887', + cadetblue: '5f9ea0', + chartreuse: '7fff00', + chocolate: 'd2691e', + coral: 'ff7f50', + cornflowerblue: '6495ed', + cornsilk: 'fff8dc', + crimson: 'dc143c', + cyan: '00ffff', + darkblue: '00008b', + darkcyan: '008b8b', + darkgoldenrod: 'b8860b', + darkgray: 'a9a9a9', + darkgreen: '006400', + darkkhaki: 'bdb76b', + darkmagenta: '8b008b', + darkolivegreen: '556b2f', + darkorange: 'ff8c00', + darkorchid: '9932cc', + darkred: '8b0000', + darksalmon: 'e9967a', + darkseagreen: '8fbc8f', + darkslateblue: '483d8b', + darkslategray: '2f4f4f', + darkturquoise: '00ced1', + darkviolet: '9400d3', + deeppink: 'ff1493', + deepskyblue: '00bfff', + dimgray: '696969', + dodgerblue: '1e90ff', + feldspar: 'd19275', + firebrick: 'b22222', + floralwhite: 'fffaf0', + forestgreen: '228b22', + fuchsia: 'ff00ff', + gainsboro: 'dcdcdc', + ghostwhite: 'f8f8ff', + gold: 'ffd700', + goldenrod: 'daa520', + gray: '808080', + green: '008000', + greenyellow: 'adff2f', + honeydew: 'f0fff0', + hotpink: 'ff69b4', + indianred: 'cd5c5c', + indigo: '4b0082', + ivory: 'fffff0', + khaki: 'f0e68c', + lavender: 'e6e6fa', + lavenderblush: 'fff0f5', + lawngreen: '7cfc00', + lemonchiffon: 'fffacd', + lightblue: 'add8e6', + lightcoral: 'f08080', + lightcyan: 'e0ffff', + lightgoldenrodyellow: 'fafad2', + lightgrey: 'd3d3d3', + lightgreen: '90ee90', + lightpink: 'ffb6c1', + lightsalmon: 'ffa07a', + lightseagreen: '20b2aa', + lightskyblue: '87cefa', + lightslateblue: '8470ff', + lightslategray: '778899', + lightsteelblue: 'b0c4de', + lightyellow: 'ffffe0', + lime: '00ff00', + limegreen: '32cd32', + linen: 'faf0e6', + magenta: 'ff00ff', + maroon: '800000', + mediumaquamarine: '66cdaa', + mediumblue: '0000cd', + mediumorchid: 'ba55d3', + mediumpurple: '9370d8', + mediumseagreen: '3cb371', + mediumslateblue: '7b68ee', + mediumspringgreen: '00fa9a', + mediumturquoise: '48d1cc', + mediumvioletred: 'c71585', + midnightblue: '191970', + mintcream: 'f5fffa', + mistyrose: 'ffe4e1', + moccasin: 'ffe4b5', + navajowhite: 'ffdead', + navy: '000080', + oldlace: 'fdf5e6', + olive: '808000', + olivedrab: '6b8e23', + orange: 'ffa500', + orangered: 'ff4500', + orchid: 'da70d6', + palegoldenrod: 'eee8aa', + palegreen: '98fb98', + paleturquoise: 'afeeee', + palevioletred: 'd87093', + papayawhip: 'ffefd5', + peachpuff: 'ffdab9', + peru: 'cd853f', + pink: 'ffc0cb', + plum: 'dda0dd', + powderblue: 'b0e0e6', + purple: '800080', + red: 'ff0000', + rosybrown: 'bc8f8f', + royalblue: '4169e1', + saddlebrown: '8b4513', + salmon: 'fa8072', + sandybrown: 'f4a460', + seagreen: '2e8b57', + seashell: 'fff5ee', + sienna: 'a0522d', + silver: 'c0c0c0', + skyblue: '87ceeb', + slateblue: '6a5acd', + slategray: '708090', + snow: 'fffafa', + springgreen: '00ff7f', + steelblue: '4682b4', + tan: 'd2b48c', + teal: '008080', + thistle: 'd8bfd8', + tomato: 'ff6347', + turquoise: '40e0d0', + violet: 'ee82ee', + violetred: 'd02090', + wheat: 'f5deb3', + white: 'ffffff', + whitesmoke: 'f5f5f5', + yellow: 'ffff00', + yellowgreen: '9acd32' + }; + + // array of color definition objects + var colorDefs = [{ + re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, + example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], + process: function process(bits) { + return [parseInt(bits[1], 10), parseInt(bits[2], 10), parseInt(bits[3], 10)]; + } + }, { + re: /^(\w{2})(\w{2})(\w{2})$/, + example: ['#00ff00', '336699'], + process: function process(bits) { + return [parseInt(bits[1], 16), parseInt(bits[2], 16), parseInt(bits[3], 16)]; + } + }, { + re: /^(\w{1})(\w{1})(\w{1})$/, + example: ['#fb0', 'f0f'], + process: function process(bits) { + return [parseInt(bits[1] + bits[1], 16), parseInt(bits[2] + bits[2], 16), parseInt(bits[3] + bits[3], 16)]; + } + }]; + + /** + * A class to parse color values + */ + + var RGBColor = function () { + /** + * @param {string} colorString + */ + function RGBColor(colorString) { + classCallCheck(this, RGBColor); + + this.ok = false; + + // strip any leading # + if (colorString.charAt(0) === '#') { + // remove # if any + colorString = colorString.substr(1, 6); + } + + colorString = colorString.replace(/ /g, ''); + colorString = colorString.toLowerCase(); + + // before getting into regexps, try simple matches + // and overwrite the input + if (colorString in simpleColors) { + colorString = simpleColors[colorString]; + } + // end of simple type-in colors + + // search through the definitions to find a match + for (var i = 0; i < colorDefs.length; i++) { + var re = colorDefs[i].re; + + var processor = colorDefs[i].process; + var bits = re.exec(colorString); + if (bits) { + var _processor = processor(bits), + _processor2 = slicedToArray(_processor, 3), + r = _processor2[0], + g = _processor2[1], + b = _processor2[2]; + + Object.assign(this, { r: r, g: g, b: b }); + this.ok = true; + } + } + + // validate/cleanup values + this.r = this.r < 0 || isNaN(this.r) ? 0 : this.r > 255 ? 255 : this.r; + this.g = this.g < 0 || isNaN(this.g) ? 0 : this.g > 255 ? 255 : this.g; + this.b = this.b < 0 || isNaN(this.b) ? 0 : this.b > 255 ? 255 : this.b; + } + + // some getters + /** + * @returns {string} + */ + + + createClass(RGBColor, [{ + key: 'toRGB', + value: function toRGB() { + return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; + } + + /** + * @returns {string} + */ + + }, { + key: 'toHex', + value: function toHex() { + var r = this.r.toString(16); + var g = this.g.toString(16); + var b = this.b.toString(16); + if (r.length === 1) { + r = '0' + r; + } + if (g.length === 1) { + g = '0' + g; + } + if (b.length === 1) { + b = '0' + b; + } + return '#' + r + g + b; + } + + /** + * help + * @returns {HTMLUListElement} + */ + + }, { + key: 'getHelpXML', + value: function getHelpXML() { + var examples = []; + // add regexps + for (var i = 0; i < colorDefs.length; i++) { + var example = colorDefs[i].example; + + for (var j = 0; j < example.length; j++) { + examples[examples.length] = example[j]; + } + } + // add type-in colors + examples.push.apply(examples, toConsumableArray(Object.keys(simpleColors))); + + var xml = document.createElement('ul'); + xml.setAttribute('id', 'rgbcolor-examples'); + for (var _i = 0; _i < examples.length; _i++) { + try { + var listItem = document.createElement('li'); + var listColor = new RGBColor(examples[_i]); + var exampleDiv = document.createElement('div'); + exampleDiv.style.cssText = 'margin: 3px;\nborder: 1px solid black;\nbackground: ' + listColor.toHex() + ';\ncolor: ' + listColor.toHex() + ';'; + exampleDiv.append('test'); + var listItemValue = ' ' + examples[_i] + ' -> ' + listColor.toRGB() + ' -> ' + listColor.toHex(); + listItem.append(exampleDiv, listItemValue); + xml.append(listItem); + } catch (e) {} + } + return xml; + } + }]); + return RGBColor; + }(); + + /** + * StackBlur - a fast almost Gaussian Blur For Canvas + + In case you find this class useful - especially in commercial projects - + I am not totally unhappy for a small donation to my PayPal account + mario@quasimondo.de + + Or support me on flattr: + https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript + + * @module StackBlur + * @version 0.5 + * @author Mario Klingemann + Contact: mario@quasimondo.com + Website: http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html + Twitter: @quasimondo + + * @copyright (c) 2010 Mario Klingemann + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + */ + + var mulTable = [512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512, 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512, 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456, 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512, 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328, 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456, 446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335, 329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512, 505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405, 399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, 324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, 268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, 451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, 385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, 332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259]; + + var shgTable = [9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24]; + + /** + * @param {string|HTMLCanvasElement} canvas + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @throws {Error} + * @returns {ImageData} See {@link https://html.spec.whatwg.org/multipage/canvas.html#imagedata} + */ + function getImageDataFromCanvas(canvas, topX, topY, width, height) { + if (typeof canvas === 'string') { + canvas = document.getElementById(canvas); + } + if (!canvas || !('getContext' in canvas)) { + return; + } + + var context = canvas.getContext('2d'); + + try { + return context.getImageData(topX, topY, width, height); + } catch (e) { + throw new Error('unable to access image data: ' + e); + } + } + + /** + * @param {HTMLCanvasElement} canvas + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @param {Float} radius + * @returns {undefined} + */ + function processCanvasRGBA(canvas, topX, topY, width, height, radius) { + if (isNaN(radius) || radius < 1) { + return; + } + radius |= 0; + + var imageData = getImageDataFromCanvas(canvas, topX, topY, width, height); + + imageData = processImageDataRGBA(imageData, topX, topY, width, height, radius); + + canvas.getContext('2d').putImageData(imageData, topX, topY); + } + + /** + * @param {ImageData} imageData + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @param {Float} radius + * @returns {ImageData} + */ + function processImageDataRGBA(imageData, topX, topY, width, height, radius) { + var pixels = imageData.data; + + var x = void 0, + y = void 0, + i = void 0, + p = void 0, + yp = void 0, + yi = void 0, + yw = void 0, + rSum = void 0, + gSum = void 0, + bSum = void 0, + aSum = void 0, + rOutSum = void 0, + gOutSum = void 0, + bOutSum = void 0, + aOutSum = void 0, + rInSum = void 0, + gInSum = void 0, + bInSum = void 0, + aInSum = void 0, + pr = void 0, + pg = void 0, + pb = void 0, + pa = void 0, + rbs = void 0; + + var div = radius + radius + 1; + // const w4 = width << 2; + var widthMinus1 = width - 1; + var heightMinus1 = height - 1; + var radiusPlus1 = radius + 1; + var sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2; + + var stackStart = new BlurStack(); + var stack = stackStart; + var stackEnd = void 0; + for (i = 1; i < div; i++) { + stack = stack.next = new BlurStack(); + if (i === radiusPlus1) { + stackEnd = stack; + } + } + stack.next = stackStart; + var stackIn = null; + var stackOut = null; + + yw = yi = 0; + + var mulSum = mulTable[radius]; + var shgSum = shgTable[radius]; + + for (y = 0; y < height; y++) { + rInSum = gInSum = bInSum = aInSum = rSum = gSum = bSum = aSum = 0; + + rOutSum = radiusPlus1 * (pr = pixels[yi]); + gOutSum = radiusPlus1 * (pg = pixels[yi + 1]); + bOutSum = radiusPlus1 * (pb = pixels[yi + 2]); + aOutSum = radiusPlus1 * (pa = pixels[yi + 3]); + + rSum += sumFactor * pr; + gSum += sumFactor * pg; + bSum += sumFactor * pb; + aSum += sumFactor * pa; + + stack = stackStart; + + for (i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + stack = stack.next; + } + + for (i = 1; i < radiusPlus1; i++) { + p = yi + ((widthMinus1 < i ? widthMinus1 : i) << 2); + rSum += (stack.r = pr = pixels[p]) * (rbs = radiusPlus1 - i); + gSum += (stack.g = pg = pixels[p + 1]) * rbs; + bSum += (stack.b = pb = pixels[p + 2]) * rbs; + aSum += (stack.a = pa = pixels[p + 3]) * rbs; + + rInSum += pr; + gInSum += pg; + bInSum += pb; + aInSum += pa; + + stack = stack.next; + } + + stackIn = stackStart; + stackOut = stackEnd; + for (x = 0; x < width; x++) { + pixels[yi + 3] = pa = aSum * mulSum >> shgSum; + if (pa !== 0) { + pa = 255 / pa; + pixels[yi] = (rSum * mulSum >> shgSum) * pa; + pixels[yi + 1] = (gSum * mulSum >> shgSum) * pa; + pixels[yi + 2] = (bSum * mulSum >> shgSum) * pa; + } else { + pixels[yi] = pixels[yi + 1] = pixels[yi + 2] = 0; + } + + rSum -= rOutSum; + gSum -= gOutSum; + bSum -= bOutSum; + aSum -= aOutSum; + + rOutSum -= stackIn.r; + gOutSum -= stackIn.g; + bOutSum -= stackIn.b; + aOutSum -= stackIn.a; + + p = yw + ((p = x + radius + 1) < widthMinus1 ? p : widthMinus1) << 2; + + rInSum += stackIn.r = pixels[p]; + gInSum += stackIn.g = pixels[p + 1]; + bInSum += stackIn.b = pixels[p + 2]; + aInSum += stackIn.a = pixels[p + 3]; + + rSum += rInSum; + gSum += gInSum; + bSum += bInSum; + aSum += aInSum; + + stackIn = stackIn.next; + + rOutSum += pr = stackOut.r; + gOutSum += pg = stackOut.g; + bOutSum += pb = stackOut.b; + aOutSum += pa = stackOut.a; + + rInSum -= pr; + gInSum -= pg; + bInSum -= pb; + aInSum -= pa; + + stackOut = stackOut.next; + + yi += 4; + } + yw += width; + } + + for (x = 0; x < width; x++) { + gInSum = bInSum = aInSum = rInSum = gSum = bSum = aSum = rSum = 0; + + yi = x << 2; + rOutSum = radiusPlus1 * (pr = pixels[yi]); + gOutSum = radiusPlus1 * (pg = pixels[yi + 1]); + bOutSum = radiusPlus1 * (pb = pixels[yi + 2]); + aOutSum = radiusPlus1 * (pa = pixels[yi + 3]); + + rSum += sumFactor * pr; + gSum += sumFactor * pg; + bSum += sumFactor * pb; + aSum += sumFactor * pa; + + stack = stackStart; + + for (i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + stack = stack.next; + } + + yp = width; + + for (i = 1; i <= radius; i++) { + yi = yp + x << 2; + + rSum += (stack.r = pr = pixels[yi]) * (rbs = radiusPlus1 - i); + gSum += (stack.g = pg = pixels[yi + 1]) * rbs; + bSum += (stack.b = pb = pixels[yi + 2]) * rbs; + aSum += (stack.a = pa = pixels[yi + 3]) * rbs; + + rInSum += pr; + gInSum += pg; + bInSum += pb; + aInSum += pa; + + stack = stack.next; + + if (i < heightMinus1) { + yp += width; + } + } + + yi = x; + stackIn = stackStart; + stackOut = stackEnd; + for (y = 0; y < height; y++) { + p = yi << 2; + pixels[p + 3] = pa = aSum * mulSum >> shgSum; + if (pa > 0) { + pa = 255 / pa; + pixels[p] = (rSum * mulSum >> shgSum) * pa; + pixels[p + 1] = (gSum * mulSum >> shgSum) * pa; + pixels[p + 2] = (bSum * mulSum >> shgSum) * pa; + } else { + pixels[p] = pixels[p + 1] = pixels[p + 2] = 0; + } + + rSum -= rOutSum; + gSum -= gOutSum; + bSum -= bOutSum; + aSum -= aOutSum; + + rOutSum -= stackIn.r; + gOutSum -= stackIn.g; + bOutSum -= stackIn.b; + aOutSum -= stackIn.a; + + p = x + ((p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1) * width << 2; + + rSum += rInSum += stackIn.r = pixels[p]; + gSum += gInSum += stackIn.g = pixels[p + 1]; + bSum += bInSum += stackIn.b = pixels[p + 2]; + aSum += aInSum += stackIn.a = pixels[p + 3]; + + stackIn = stackIn.next; + + rOutSum += pr = stackOut.r; + gOutSum += pg = stackOut.g; + bOutSum += pb = stackOut.b; + aOutSum += pa = stackOut.a; + + rInSum -= pr; + gInSum -= pg; + bInSum -= pb; + aInSum -= pa; + + stackOut = stackOut.next; + + yi += width; + } + } + return imageData; + } + + /** + * + */ + var BlurStack = function BlurStack() { + classCallCheck(this, BlurStack); + + this.r = 0; + this.g = 0; + this.b = 0; + this.a = 0; + this.next = null; + }; + + /* eslint-disable new-cap */ + + var canvasRGBA_ = processCanvasRGBA; + + /** + * @callback module:canvg.StackBlurCanvasRGBA + * @param {string} id + * @param {Float} x + * @param {Float} y + * @param {Float} width + * @param {Float} height + * @param {Float} blurRadius + */ + + /** + * @callback module:canvg.ForceRedraw + * @returns {boolean} + */ + + /** + * @function module:canvg.setStackBlurCanvasRGBA + * @param {module:canvg.StackBlurCanvasRGBA} cb Will be passed the canvas ID, x, y, width, height, blurRadius + */ + var setStackBlurCanvasRGBA = function setStackBlurCanvasRGBA(cb) { + canvasRGBA_ = cb; + }; + + /** + * @typedef {PlainObject} module:canvg.CanvgOptions + * @property {boolean} opts.ignoreMouse true => ignore mouse events + * @property {boolean} opts.ignoreAnimation true => ignore animations + * @property {boolean} opts.ignoreDimensions true => does not try to resize canvas + * @property {boolean} opts.ignoreClear true => does not clear canvas + * @property {Integer} opts.offsetX int => draws at a x offset + * @property {Integer} opts.offsetY int => draws at a y offset + * @property {Integer} opts.scaleWidth int => scales horizontally to width + * @property {Integer} opts.scaleHeight int => scales vertically to height + * @property {module:canvg.ForceRedraw} opts.forceRedraw function => will call the function on every frame, if it returns true, will redraw + * @property {boolean} opts.log Adds log function + * @property {boolean} opts.useCORS Whether to set CORS `crossOrigin` for the image to `Anonymous` + */ + + /** + * If called with no arguments, it will replace all `` elements on the page with `` elements + * @function module:canvg.canvg + * @param {HTMLCanvasElement|string} target canvas element or the id of a canvas element + * @param {string|XMLDocument} s: svg string, url to svg file, or xml document + * @param {module:canvg.CanvgOptions} [opts] Optional hash of options + * @returns {Promise} All the function after the first render is completed with dom + */ + var canvg = function canvg(target, s, opts) { + // no parameters + if (target == null && s == null && opts == null) { + var svgTags = document.querySelectorAll('svg'); + return Promise.all([].concat(toConsumableArray(svgTags)).map(function (svgTag) { + var c = document.createElement('canvas'); + c.width = svgTag.clientWidth; + c.height = svgTag.clientHeight; + svgTag.before(c); + svgTag.remove(); + var div = document.createElement('div'); + div.append(svgTag); + return canvg(c, div.innerHTML); + })); + } + + if (typeof target === 'string') { + target = document.getElementById(target); + } + + // store class on canvas + if (target.svg != null) target.svg.stop(); + var svg = build(opts || {}); + // on i.e. 8 for flash canvas, we can't assign the property so check for it + if (!(target.childNodes.length === 1 && target.childNodes[0].nodeName === 'OBJECT')) { + target.svg = svg; + } + + var ctx = target.getContext('2d'); + if (typeof s.documentElement !== 'undefined') { + // load from xml doc + return svg.loadXmlDoc(ctx, s); + } + if (s.substr(0, 1) === '<') { + // load from xml string + return svg.loadXml(ctx, s); + } + // load from url + return svg.load(ctx, s); + }; + + /** + * @param {module:canvg.CanvgOptions} opts + * @returns {object} + * @todo Flesh out exactly what object is returned here (after updating to latest and reincluding our changes here and those of StackBlur) + */ + function build(opts) { + var svg = { opts: opts }; + + svg.FRAMERATE = 30; + svg.MAX_VIRTUAL_PIXELS = 30000; + + svg.log = function (msg) {}; + if (svg.opts.log === true && typeof console !== 'undefined') { + svg.log = function (msg) { + console.log(msg); + }; + } + + // globals + svg.init = function (ctx) { + var uniqueId = 0; + svg.UniqueId = function () { + uniqueId++; + return 'canvg' + uniqueId; + }; + svg.Definitions = {}; + svg.Styles = {}; + svg.Animations = []; + svg.Images = []; + svg.ctx = ctx; + svg.ViewPort = { + viewPorts: [], + Clear: function Clear() { + this.viewPorts = []; + }, + SetCurrent: function SetCurrent(width, height) { + this.viewPorts.push({ width: width, height: height }); + }, + RemoveCurrent: function RemoveCurrent() { + this.viewPorts.pop(); + }, + Current: function Current() { + return this.viewPorts[this.viewPorts.length - 1]; + }, + width: function width() { + return this.Current().width; + }, + height: function height() { + return this.Current().height; + }, + ComputeSize: function ComputeSize(d) { + if (d != null && typeof d === 'number') return d; + if (d === 'x') return this.width(); + if (d === 'y') return this.height(); + return Math.sqrt(Math.pow(this.width(), 2) + Math.pow(this.height(), 2)) / Math.sqrt(2); + } + }; + }; + svg.init(); + + // images loaded + svg.ImagesLoaded = function () { + return svg.Images.every(function (img) { + return img.loaded; + }); + }; + + // trim + svg.trim = function (s) { + return s.replace(/^\s+|\s+$/g, ''); + }; + + // compress spaces + svg.compressSpaces = function (s) { + return s.replace(/[\s\r\t\n]+/gm, ' '); + }; + + // ajax + svg.ajax = function (url, asynch) { + var AJAX = window.XMLHttpRequest ? new XMLHttpRequest() : new window.ActiveXObject('Microsoft.XMLHTTP'); + if (!AJAX) { + return null; + } + if (asynch) { + return new Promise(function (resolve, reject) { + var req = AJAX.open('GET', url, true); + req.addEventListener('load', function () { + resolve(AJAX.responseText); + }); + AJAX.send(null); + }); + } + + AJAX.open('GET', url, false); + AJAX.send(null); + return AJAX.responseText; + }; + + // parse xml + svg.parseXml = function (xml) { + if (window.DOMParser) { + var parser = new DOMParser(); + return parser.parseFromString(xml, 'text/xml'); + } else { + xml = xml.replace(/]*>/, ''); + var xmlDoc = new window.ActiveXObject('Microsoft.XMLDOM'); + xmlDoc.async = 'false'; + xmlDoc.loadXML(xml); + return xmlDoc; + } + }; + + // text extensions + // get the text baseline + var textBaselineMapping = { + baseline: 'alphabetic', + 'before-edge': 'top', + 'text-before-edge': 'top', + middle: 'middle', + central: 'middle', + 'after-edge': 'bottom', + 'text-after-edge': 'bottom', + ideographic: 'ideographic', + alphabetic: 'alphabetic', + hanging: 'hanging', + mathematical: 'alphabetic' + }; + + svg.Property = function () { + function Property(name, value) { + classCallCheck(this, Property); + + this.name = name; + this.value = value; + } + + createClass(Property, [{ + key: 'getValue', + value: function getValue() { + return this.value; + } + }, { + key: 'hasValue', + value: function hasValue() { + return this.value != null && this.value !== ''; + } + + // return the numerical value of the property + + }, { + key: 'numValue', + value: function numValue() { + if (!this.hasValue()) return 0; + + var n = parseFloat(this.value); + if ((this.value + '').match(/%$/)) { + n = n / 100.0; + } + return n; + } + }, { + key: 'valueOrDefault', + value: function valueOrDefault(def) { + if (this.hasValue()) return this.value; + return def; + } + }, { + key: 'numValueOrDefault', + value: function numValueOrDefault(def) { + if (this.hasValue()) return this.numValue(); + return def; + } + + // color extensions + // augment the current color value with the opacity + + }, { + key: 'addOpacity', + value: function addOpacity(opacityProp) { + var newValue = this.value; + if (opacityProp.value != null && opacityProp.value !== '' && typeof this.value === 'string') { + // can only add opacity to colors, not patterns + var color = new RGBColor(this.value); + if (color.ok) { + newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacityProp.numValue() + ')'; + } + } + return new svg.Property(this.name, newValue); + } + + // definition extensions + // get the definition from the definitions table + + }, { + key: 'getDefinition', + value: function getDefinition() { + var name = this.value.match(/#([^)'"]+)/); + if (name) { + name = name[1]; + } + if (!name) { + name = this.value; + } + return svg.Definitions[name]; + } + }, { + key: 'isUrlDefinition', + value: function isUrlDefinition() { + return this.value.startsWith('url('); + } + }, { + key: 'getFillStyleDefinition', + value: function getFillStyleDefinition(e, opacityProp) { + var def = this.getDefinition(); + + // gradient + if (def != null && def.createGradient) { + return def.createGradient(svg.ctx, e, opacityProp); + } + + // pattern + if (def != null && def.createPattern) { + if (def.getHrefAttribute().hasValue()) { + var pt = def.attribute('patternTransform'); + def = def.getHrefAttribute().getDefinition(); + if (pt.hasValue()) { + def.attribute('patternTransform', true).value = pt.value; + } + } + return def.createPattern(svg.ctx, e); + } + + return null; + } + + // length extensions + + }, { + key: 'getDPI', + value: function getDPI(viewPort) { + return 96.0; // TODO: compute? + } + }, { + key: 'getEM', + value: function getEM(viewPort) { + var em = 12; + + var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + if (fontSize.hasValue()) em = fontSize.toPixels(viewPort); + + return em; + } + }, { + key: 'getUnits', + value: function getUnits() { + var s = this.value + ''; + return s.replace(/[0-9.-]/g, ''); + } + + // get the length as pixels + + }, { + key: 'toPixels', + value: function toPixels(viewPort, processPercent) { + if (!this.hasValue()) return 0; + var s = this.value + ''; + if (s.match(/em$/)) return this.numValue() * this.getEM(viewPort); + if (s.match(/ex$/)) return this.numValue() * this.getEM(viewPort) / 2.0; + if (s.match(/px$/)) return this.numValue(); + if (s.match(/pt$/)) return this.numValue() * this.getDPI(viewPort) * (1.0 / 72.0); + if (s.match(/pc$/)) return this.numValue() * 15; + if (s.match(/cm$/)) return this.numValue() * this.getDPI(viewPort) / 2.54; + if (s.match(/mm$/)) return this.numValue() * this.getDPI(viewPort) / 25.4; + if (s.match(/in$/)) return this.numValue() * this.getDPI(viewPort); + if (s.match(/%$/)) return this.numValue() * svg.ViewPort.ComputeSize(viewPort); + var n = this.numValue(); + if (processPercent && n < 1.0) return n * svg.ViewPort.ComputeSize(viewPort); + return n; + } + + // time extensions + // get the time as milliseconds + + }, { + key: 'toMilliseconds', + value: function toMilliseconds() { + if (!this.hasValue()) return 0; + var s = this.value + ''; + if (s.match(/s$/)) return this.numValue() * 1000; + if (s.match(/ms$/)) return this.numValue(); + return this.numValue(); + } + + // angle extensions + // get the angle as radians + + }, { + key: 'toRadians', + value: function toRadians() { + if (!this.hasValue()) return 0; + var s = this.value + ''; + if (s.match(/deg$/)) return this.numValue() * (Math.PI / 180.0); + if (s.match(/grad$/)) return this.numValue() * (Math.PI / 200.0); + if (s.match(/rad$/)) return this.numValue(); + return this.numValue() * (Math.PI / 180.0); + } + }, { + key: 'toTextBaseline', + value: function toTextBaseline() { + if (!this.hasValue()) return null; + return textBaselineMapping[this.value]; + } + }]); + return Property; + }(); + + // fonts + svg.Font = { + Styles: 'normal|italic|oblique|inherit', + Variants: 'normal|small-caps|inherit', + Weights: 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit', + + CreateFont: function CreateFont(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) { + var f = inherit != null ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font); + return { + fontFamily: fontFamily || f.fontFamily, + fontSize: fontSize || f.fontSize, + fontStyle: fontStyle || f.fontStyle, + fontWeight: fontWeight || f.fontWeight, + fontVariant: fontVariant || f.fontVariant, + toString: function toString() { + return [this.fontStyle, this.fontVariant, this.fontWeight, this.fontSize, this.fontFamily].join(' '); + } + }; + }, + Parse: function Parse(s) { + var _this = this; + + var f = {}; + var d = svg.trim(svg.compressSpaces(s || '')).split(' '); + var set$$1 = { + fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false + }; + var ff = ''; + d.forEach(function (d) { + if (!set$$1.fontStyle && _this.Styles.includes(d)) { + if (d !== 'inherit') { + f.fontStyle = d; + } + set$$1.fontStyle = true; + } else if (!set$$1.fontVariant && _this.Variants.includes(d)) { + if (d !== 'inherit') { + f.fontVariant = d; + } + set$$1.fontStyle = set$$1.fontVariant = true; + } else if (!set$$1.fontWeight && _this.Weights.includes(d)) { + if (d !== 'inherit') { + f.fontWeight = d; + } + set$$1.fontStyle = set$$1.fontVariant = set$$1.fontWeight = true; + } else if (!set$$1.fontSize) { + if (d !== 'inherit') { + f.fontSize = d.split('/')[0]; + } + set$$1.fontStyle = set$$1.fontVariant = set$$1.fontWeight = set$$1.fontSize = true; + } else { + if (d !== 'inherit') { + ff += d; + } + } + }); + if (ff !== '') { + f.fontFamily = ff; + } + return f; + } + }; + + // points and paths + svg.ToNumberArray = function (s) { + var a = svg.trim(svg.compressSpaces((s || '').replace(/,/g, ' '))).split(' '); + return a.map(function (a) { + return parseFloat(a); + }); + }; + svg.Point = function () { + function _class(x, y) { + classCallCheck(this, _class); + + this.x = x; + this.y = y; + } + + createClass(_class, [{ + key: 'angleTo', + value: function angleTo(p) { + return Math.atan2(p.y - this.y, p.x - this.x); + } + }, { + key: 'applyTransform', + value: function applyTransform(v) { + var xp = this.x * v[0] + this.y * v[2] + v[4]; + var yp = this.x * v[1] + this.y * v[3] + v[5]; + this.x = xp; + this.y = yp; + } + }]); + return _class; + }(); + + svg.CreatePoint = function (s) { + var a = svg.ToNumberArray(s); + return new svg.Point(a[0], a[1]); + }; + svg.CreatePath = function (s) { + var a = svg.ToNumberArray(s); + var path = []; + for (var i = 0; i < a.length; i += 2) { + path.push(new svg.Point(a[i], a[i + 1])); + } + return path; + }; + + // bounding box + svg.BoundingBox = function () { + function _class2(x1, y1, x2, y2) { + classCallCheck(this, _class2); + // pass in initial points if you want + this.x1 = Number.NaN; + this.y1 = Number.NaN; + this.x2 = Number.NaN; + this.y2 = Number.NaN; + this.addPoint(x1, y1); + this.addPoint(x2, y2); + } + + createClass(_class2, [{ + key: 'x', + value: function x() { + return this.x1; + } + }, { + key: 'y', + value: function y() { + return this.y1; + } + }, { + key: 'width', + value: function width() { + return this.x2 - this.x1; + } + }, { + key: 'height', + value: function height() { + return this.y2 - this.y1; + } + }, { + key: 'addPoint', + value: function addPoint(x, y) { + if (x != null) { + if (isNaN(this.x1) || isNaN(this.x2)) { + this.x1 = x; + this.x2 = x; + } + if (x < this.x1) this.x1 = x; + if (x > this.x2) this.x2 = x; + } + + if (y != null) { + if (isNaN(this.y1) || isNaN(this.y2)) { + this.y1 = y; + this.y2 = y; + } + if (y < this.y1) this.y1 = y; + if (y > this.y2) this.y2 = y; + } + } + }, { + key: 'addX', + value: function addX(x) { + this.addPoint(x, null); + } + }, { + key: 'addY', + value: function addY(y) { + this.addPoint(null, y); + } + }, { + key: 'addBoundingBox', + value: function addBoundingBox(bb) { + this.addPoint(bb.x1, bb.y1); + this.addPoint(bb.x2, bb.y2); + } + }, { + key: 'addQuadraticCurve', + value: function addQuadraticCurve(p0x, p0y, p1x, p1y, p2x, p2y) { + var cp1x = p0x + 2 / 3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) + var cp1y = p0y + 2 / 3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) + var cp2x = cp1x + 1 / 3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) + var cp2y = cp1y + 1 / 3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) + this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); + } + }, { + key: 'addBezierCurve', + value: function addBezierCurve(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { + var _this2 = this; + + // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html + var p0 = [p0x, p0y], + p1 = [p1x, p1y], + p2 = [p2x, p2y], + p3 = [p3x, p3y]; + this.addPoint(p0[0], p0[1]); + this.addPoint(p3[0], p3[1]); + + var _loop = function _loop(i) { + var f = function f(t) { + return Math.pow(1 - t, 3) * p0[i] + 3 * Math.pow(1 - t, 2) * t * p1[i] + 3 * (1 - t) * Math.pow(t, 2) * p2[i] + Math.pow(t, 3) * p3[i]; + }; + + var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; + var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; + var c = 3 * p1[i] - 3 * p0[i]; + + if (a === 0) { + if (b === 0) return 'continue'; + var t = -c / b; + if (t > 0 && t < 1) { + if (i === 0) _this2.addX(f(t)); + if (i === 1) _this2.addY(f(t)); + } + return 'continue'; + } + + var b2ac = Math.pow(b, 2) - 4 * c * a; + if (b2ac < 0) return 'continue'; + var t1 = (-b + Math.sqrt(b2ac)) / (2 * a); + if (t1 > 0 && t1 < 1) { + if (i === 0) _this2.addX(f(t1)); + if (i === 1) _this2.addY(f(t1)); + } + var t2 = (-b - Math.sqrt(b2ac)) / (2 * a); + if (t2 > 0 && t2 < 1) { + if (i === 0) _this2.addX(f(t2)); + if (i === 1) _this2.addY(f(t2)); + } + }; + + for (var i = 0; i <= 1; i++) { + var _ret = _loop(i); + + if (_ret === 'continue') continue; + } + } + }, { + key: 'isPointInBox', + value: function isPointInBox(x, y) { + return this.x1 <= x && x <= this.x2 && this.y1 <= y && y <= this.y2; + } + }]); + return _class2; + }(); + + // transforms + svg.Transform = function () { + function _class3(v) { + var _this6 = this; + + classCallCheck(this, _class3); + + this.Type = { + translate: function translate(s) { + classCallCheck(this, translate); + + this.p = svg.CreatePoint(s); + this.apply = function (ctx) { + ctx.translate(this.p.x || 0.0, this.p.y || 0.0); + }; + this.unapply = function (ctx) { + ctx.translate(-1.0 * this.p.x || 0.0, -1.0 * this.p.y || 0.0); + }; + this.applyToPoint = function (p) { + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + }; + }, + rotate: function rotate(s) { + classCallCheck(this, rotate); + + var a = svg.ToNumberArray(s); + this.angle = new svg.Property('angle', a[0]); + this.cx = a[1] || 0; + this.cy = a[2] || 0; + this.apply = function (ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(this.angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + }; + this.unapply = function (ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(-1.0 * this.angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + }; + this.applyToPoint = function (p) { + var a = this.angle.toRadians(); + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + p.applyTransform([Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0]); + p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]); + }; + }, + scale: function scale(s) { + classCallCheck(this, scale); + + this.p = svg.CreatePoint(s); + this.apply = function (ctx) { + ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0); + }; + this.unapply = function (ctx) { + ctx.scale(1.0 / this.p.x || 1.0, 1.0 / this.p.y || this.p.x || 1.0); + }; + this.applyToPoint = function (p) { + p.applyTransform([this.p.x || 0.0, 0, 0, this.p.y || 0.0, 0, 0]); + }; + }, + matrix: function matrix(s) { + classCallCheck(this, matrix); + + this.m = svg.ToNumberArray(s); + this.apply = function (ctx) { + ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]); + }; + this.applyToPoint = function (p) { + p.applyTransform(this.m); + }; + } + }; + Object.assign(this.Type, { + SkewBase: function (_Type$matrix) { + inherits(SkewBase, _Type$matrix); + + function SkewBase(s) { + classCallCheck(this, SkewBase); + + var _this3 = possibleConstructorReturn(this, (SkewBase.__proto__ || Object.getPrototypeOf(SkewBase)).call(this, s)); + + _this3.angle = new svg.Property('angle', s); + return _this3; + } + + return SkewBase; + }(this.Type.matrix) + }); + Object.assign(this.Type, { + skewX: function (_Type$SkewBase) { + inherits(skewX, _Type$SkewBase); + + function skewX(s) { + classCallCheck(this, skewX); + + var _this4 = possibleConstructorReturn(this, (skewX.__proto__ || Object.getPrototypeOf(skewX)).call(this, s)); + + _this4.m = [1, 0, Math.tan(_this4.angle.toRadians()), 1, 0, 0]; + return _this4; + } + + return skewX; + }(this.Type.SkewBase), + skewY: function (_Type$SkewBase2) { + inherits(skewY, _Type$SkewBase2); + + function skewY(s) { + classCallCheck(this, skewY); + + var _this5 = possibleConstructorReturn(this, (skewY.__proto__ || Object.getPrototypeOf(skewY)).call(this, s)); + + _this5.m = [1, Math.tan(_this5.angle.toRadians()), 0, 1, 0, 0]; + return _this5; + } + + return skewY; + }(this.Type.SkewBase) + }); + + var data = svg.trim(svg.compressSpaces(v)).replace(/\)([a-zA-Z])/g, ') $1').replace(/\)(\s?,\s?)/g, ') ').split(/\s(?=[a-z])/); + this.transforms = data.map(function (d) { + var type = svg.trim(d.split('(')[0]); + var s = d.split('(')[1].replace(')', ''); + var transform = new _this6.Type[type](s); + transform.type = type; + return transform; + }); + } + + createClass(_class3, [{ + key: 'apply', + value: function apply(ctx) { + this.transforms.forEach(function (transform) { + transform.apply(ctx); + }); + } + }, { + key: 'unapply', + value: function unapply(ctx) { + for (var i = this.transforms.length - 1; i >= 0; i--) { + this.transforms[i].unapply(ctx); + } + } + }, { + key: 'applyToPoint', + value: function applyToPoint(p) { + this.transforms.forEach(function (transform) { + transform.applyToPoint(p); + }); + } + }]); + return _class3; + }(); + + // aspect ratio + svg.AspectRatio = function (ctx, aspectRatio, width, desiredWidth, height, desiredHeight, minX, minY, refX, refY) { + // aspect ratio - https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute + aspectRatio = svg.compressSpaces(aspectRatio); + aspectRatio = aspectRatio.replace(/^defer\s/, ''); // ignore defer + var align = aspectRatio.split(' ')[0] || 'xMidYMid'; + var meetOrSlice = aspectRatio.split(' ')[1] || 'meet'; + + // calculate scale + var scaleX = width / desiredWidth; + var scaleY = height / desiredHeight; + var scaleMin = Math.min(scaleX, scaleY); + var scaleMax = Math.max(scaleX, scaleY); + if (meetOrSlice === 'meet') { + desiredWidth *= scaleMin;desiredHeight *= scaleMin; + } + if (meetOrSlice === 'slice') { + desiredWidth *= scaleMax;desiredHeight *= scaleMax; + } + + refX = new svg.Property('refX', refX); + refY = new svg.Property('refY', refY); + if (refX.hasValue() && refY.hasValue()) { + ctx.translate(-scaleMin * refX.toPixels('x'), -scaleMin * refY.toPixels('y')); + } else { + // align + if (align.match(/^xMid/) && (meetOrSlice === 'meet' && scaleMin === scaleY || meetOrSlice === 'slice' && scaleMax === scaleY)) ctx.translate(width / 2.0 - desiredWidth / 2.0, 0); + if (align.match(/YMid$/) && (meetOrSlice === 'meet' && scaleMin === scaleX || meetOrSlice === 'slice' && scaleMax === scaleX)) ctx.translate(0, height / 2.0 - desiredHeight / 2.0); + if (align.match(/^xMax/) && (meetOrSlice === 'meet' && scaleMin === scaleY || meetOrSlice === 'slice' && scaleMax === scaleY)) ctx.translate(width - desiredWidth, 0); + if (align.match(/YMax$/) && (meetOrSlice === 'meet' && scaleMin === scaleX || meetOrSlice === 'slice' && scaleMax === scaleX)) ctx.translate(0, height - desiredHeight); + } + + // scale + if (align === 'none') ctx.scale(scaleX, scaleY);else if (meetOrSlice === 'meet') ctx.scale(scaleMin, scaleMin);else if (meetOrSlice === 'slice') ctx.scale(scaleMax, scaleMax); + + // translate + ctx.translate(minX == null ? 0 : -minX, minY == null ? 0 : -minY); + }; + + // elements + svg.Element = {}; + + svg.EmptyProperty = new svg.Property('EMPTY', ''); + + svg.Element.ElementBase = function () { + function _class4(node) { + var _this7 = this; + + classCallCheck(this, _class4); + + this.captureTextNodes = arguments[1]; // Argument from inheriting class + this.attributes = {}; + this.styles = {}; + this.children = []; + if (node != null && node.nodeType === 1) { + // ELEMENT_NODE + // add children + [].concat(toConsumableArray(node.childNodes)).forEach(function (childNode) { + if (childNode.nodeType === 1) { + _this7.addChild(childNode, true); // ELEMENT_NODE + } + if (_this7.captureTextNodes && (childNode.nodeType === 3 || childNode.nodeType === 4)) { + var text = childNode.nodeValue || childNode.text || ''; + if (svg.trim(svg.compressSpaces(text)) !== '') { + _this7.addChild(new svg.Element.tspan(childNode), false); // TEXT_NODE + } + } + }); + + // add attributes + [].concat(toConsumableArray(node.attributes)).forEach(function (_ref) { + var nodeName = _ref.nodeName, + nodeValue = _ref.nodeValue; + + _this7.attributes[nodeName] = new svg.Property(nodeName, nodeValue); + }); + // add tag styles + var styles = svg.Styles[node.nodeName]; + if (styles != null) { + for (var name in styles) { + this.styles[name] = styles[name]; + } + } + + // add class styles + if (this.attribute('class').hasValue()) { + var classes = svg.compressSpaces(this.attribute('class').value).split(' '); + classes.forEach(function (clss) { + styles = svg.Styles['.' + clss]; + if (styles != null) { + for (var _name in styles) { + _this7.styles[_name] = styles[_name]; + } + } + styles = svg.Styles[node.nodeName + '.' + clss]; + if (styles != null) { + for (var _name2 in styles) { + _this7.styles[_name2] = styles[_name2]; + } + } + }); + } + + // add id styles + if (this.attribute('id').hasValue()) { + var _styles = svg.Styles['#' + this.attribute('id').value]; + if (_styles != null) { + for (var _name3 in _styles) { + this.styles[_name3] = _styles[_name3]; + } + } + } + + // add inline styles + if (this.attribute('style').hasValue()) { + var _styles2 = this.attribute('style').value.split(';'); + _styles2.forEach(function (style) { + if (svg.trim(style) !== '') { + var _style$split = style.split(':'), + _name4 = _style$split.name, + value = _style$split.value; + + _name4 = svg.trim(_name4); + value = svg.trim(value); + _this7.styles[_name4] = new svg.Property(_name4, value); + } + }); + } + + // add id + if (this.attribute('id').hasValue()) { + if (svg.Definitions[this.attribute('id').value] == null) { + svg.Definitions[this.attribute('id').value] = this; + } + } + } + } + + // get or create attribute + + + createClass(_class4, [{ + key: 'attribute', + value: function attribute(name, createIfNotExists) { + var a = this.attributes[name]; + if (a != null) return a; + + if (createIfNotExists === true) { + a = new svg.Property(name, '');this.attributes[name] = a; + } + return a || svg.EmptyProperty; + } + }, { + key: 'getHrefAttribute', + value: function getHrefAttribute() { + for (var a in this.attributes) { + if (a.match(/:href$/)) { + return this.attributes[a]; + } + } + return svg.EmptyProperty; + } + + // get or create style, crawls up node tree + + }, { + key: 'style', + value: function style(name, createIfNotExists, skipAncestors) { + var s = this.styles[name]; + if (s != null) return s; + + var a = this.attribute(name); + if (a != null && a.hasValue()) { + this.styles[name] = a; // move up to me to cache + return a; + } + + if (skipAncestors !== true) { + var p = this.parent; + if (p != null) { + var ps = p.style(name); + if (ps != null && ps.hasValue()) { + return ps; + } + } + } + + if (createIfNotExists === true) { + s = new svg.Property(name, '');this.styles[name] = s; + } + return s || svg.EmptyProperty; + } + + // base render + + }, { + key: 'render', + value: function render(ctx) { + // don't render display=none + if (this.style('display').value === 'none') return; + + // don't render visibility=hidden + if (this.style('visibility').value === 'hidden') return; + + ctx.save(); + if (this.attribute('mask').hasValue()) { + // mask + var mask = this.attribute('mask').getDefinition(); + if (mask != null) mask.apply(ctx, this); + } else if (this.style('filter').hasValue()) { + // filter + var filter = this.style('filter').getDefinition(); + if (filter != null) filter.apply(ctx, this); + } else { + this.setContext(ctx); + this.renderChildren(ctx); + this.clearContext(ctx); + } + ctx.restore(); + } + + // base set context + + }, { + key: 'setContext', + value: function setContext(ctx) {} + // OVERRIDE ME! + + + // base clear context + + }, { + key: 'clearContext', + value: function clearContext(ctx) {} + // OVERRIDE ME! + + + // base render children + + }, { + key: 'renderChildren', + value: function renderChildren(ctx) { + this.children.forEach(function (child) { + child.render(ctx); + }); + } + }, { + key: 'addChild', + value: function addChild(childNode, create) { + var child = create ? svg.CreateElement(childNode) : childNode; + child.parent = this; + if (child.type !== 'title') { + this.children.push(child); + } + } + }]); + return _class4; + }(); + + svg.Element.RenderedElementBase = function (_svg$Element$ElementB) { + inherits(_class5, _svg$Element$ElementB); + + function _class5() { + classCallCheck(this, _class5); + return possibleConstructorReturn(this, (_class5.__proto__ || Object.getPrototypeOf(_class5)).apply(this, arguments)); + } + + createClass(_class5, [{ + key: 'setContext', + value: function setContext(ctx) { + // fill + if (this.style('fill').isUrlDefinition()) { + var fs = this.style('fill').getFillStyleDefinition(this, this.style('fill-opacity')); + if (fs != null) ctx.fillStyle = fs; + } else if (this.style('fill').hasValue()) { + var fillStyle = this.style('fill'); + if (fillStyle.value === 'currentColor') fillStyle.value = this.style('color').value; + ctx.fillStyle = fillStyle.value === 'none' ? 'rgba(0,0,0,0)' : fillStyle.value; + } + if (this.style('fill-opacity').hasValue()) { + var _fillStyle = new svg.Property('fill', ctx.fillStyle); + _fillStyle = _fillStyle.addOpacity(this.style('fill-opacity')); + ctx.fillStyle = _fillStyle.value; + } + + // stroke + if (this.style('stroke').isUrlDefinition()) { + var _fs = this.style('stroke').getFillStyleDefinition(this, this.style('stroke-opacity')); + if (_fs != null) ctx.strokeStyle = _fs; + } else if (this.style('stroke').hasValue()) { + var strokeStyle = this.style('stroke'); + if (strokeStyle.value === 'currentColor') strokeStyle.value = this.style('color').value; + ctx.strokeStyle = strokeStyle.value === 'none' ? 'rgba(0,0,0,0)' : strokeStyle.value; + } + if (this.style('stroke-opacity').hasValue()) { + var _strokeStyle = new svg.Property('stroke', ctx.strokeStyle); + _strokeStyle = _strokeStyle.addOpacity(this.style('stroke-opacity')); + ctx.strokeStyle = _strokeStyle.value; + } + if (this.style('stroke-width').hasValue()) { + var newLineWidth = this.style('stroke-width').toPixels(); + ctx.lineWidth = newLineWidth === 0 ? 0.001 : newLineWidth; // browsers don't respect 0 + } + if (this.style('stroke-linecap').hasValue()) ctx.lineCap = this.style('stroke-linecap').value; + if (this.style('stroke-linejoin').hasValue()) ctx.lineJoin = this.style('stroke-linejoin').value; + if (this.style('stroke-miterlimit').hasValue()) ctx.miterLimit = this.style('stroke-miterlimit').value; + if (this.style('stroke-dasharray').hasValue() && this.style('stroke-dasharray').value !== 'none') { + var gaps = svg.ToNumberArray(this.style('stroke-dasharray').value); + if (typeof ctx.setLineDash !== 'undefined') { + ctx.setLineDash(gaps); + } else if (typeof ctx.webkitLineDash !== 'undefined') { + ctx.webkitLineDash = gaps; + } else if (typeof ctx.mozDash !== 'undefined' && !(gaps.length === 1 && gaps[0] === 0)) { + ctx.mozDash = gaps; + } + + var offset = this.style('stroke-dashoffset').numValueOrDefault(1); + if (typeof ctx.lineDashOffset !== 'undefined') { + ctx.lineDashOffset = offset; + } else if (typeof ctx.webkitLineDashOffset !== 'undefined') { + ctx.webkitLineDashOffset = offset; + } else if (typeof ctx.mozDashOffset !== 'undefined') { + ctx.mozDashOffset = offset; + } + } + + // font + if (typeof ctx.font !== 'undefined') { + ctx.font = svg.Font.CreateFont(this.style('font-style').value, this.style('font-variant').value, this.style('font-weight').value, this.style('font-size').hasValue() ? this.style('font-size').toPixels() + 'px' : '', this.style('font-family').value).toString(); + } + + // transform + if (this.attribute('transform').hasValue()) { + var transform = new svg.Transform(this.attribute('transform').value); + transform.apply(ctx); + } + + // clip + if (this.style('clip-path', false, true).hasValue()) { + var clip = this.style('clip-path', false, true).getDefinition(); + if (clip != null) clip.apply(ctx); + } + + // opacity + if (this.style('opacity').hasValue()) { + ctx.globalAlpha = this.style('opacity').numValue(); + } + } + }]); + return _class5; + }(svg.Element.ElementBase); + + svg.Element.PathElementBase = function (_svg$Element$Rendered) { + inherits(_class6, _svg$Element$Rendered); + + function _class6() { + classCallCheck(this, _class6); + return possibleConstructorReturn(this, (_class6.__proto__ || Object.getPrototypeOf(_class6)).apply(this, arguments)); + } + + createClass(_class6, [{ + key: 'path', + value: function path(ctx) { + if (ctx != null) ctx.beginPath(); + return new svg.BoundingBox(); + } + }, { + key: 'renderChildren', + value: function renderChildren(ctx) { + this.path(ctx); + svg.Mouse.checkPath(this, ctx); + if (ctx.fillStyle !== '') { + if (this.style('fill-rule').valueOrDefault('inherit') !== 'inherit') { + ctx.fill(this.style('fill-rule').value); + } else { + ctx.fill(); + } + } + if (ctx.strokeStyle !== '') ctx.stroke(); + + var markers = this.getMarkers(); + if (markers != null) { + if (this.style('marker-start').isUrlDefinition()) { + var marker = this.style('marker-start').getDefinition(); + marker.render(ctx, markers[0][0], markers[0][1]); + } + if (this.style('marker-mid').isUrlDefinition()) { + var _marker = this.style('marker-mid').getDefinition(); + for (var i = 1; i < markers.length - 1; i++) { + _marker.render(ctx, markers[i][0], markers[i][1]); + } + } + if (this.style('marker-end').isUrlDefinition()) { + var _marker2 = this.style('marker-end').getDefinition(); + _marker2.render(ctx, markers[markers.length - 1][0], markers[markers.length - 1][1]); + } + } + } + }, { + key: 'getBoundingBox', + value: function getBoundingBox() { + return this.path(); + } + }, { + key: 'getMarkers', + value: function getMarkers() { + return null; + } + }]); + return _class6; + }(svg.Element.RenderedElementBase); + + // svg element + svg.Element.svg = function (_svg$Element$Rendered2) { + inherits(_class7, _svg$Element$Rendered2); + + function _class7() { + classCallCheck(this, _class7); + return possibleConstructorReturn(this, (_class7.__proto__ || Object.getPrototypeOf(_class7)).apply(this, arguments)); + } + + createClass(_class7, [{ + key: 'clearContext', + value: function clearContext(ctx) { + get(_class7.prototype.__proto__ || Object.getPrototypeOf(_class7.prototype), 'clearContext', this).call(this, ctx); + svg.ViewPort.RemoveCurrent(); + } + }, { + key: 'setContext', + value: function setContext(ctx) { + // initial values and defaults + ctx.strokeStyle = 'rgba(0,0,0,0)'; + ctx.lineCap = 'butt'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 4; + if (typeof ctx.font !== 'undefined' && typeof window.getComputedStyle !== 'undefined') { + ctx.font = window.getComputedStyle(ctx.canvas).getPropertyValue('font'); + } + + get(_class7.prototype.__proto__ || Object.getPrototypeOf(_class7.prototype), 'setContext', this).call(this, ctx); + + // create new view port + if (!this.attribute('x').hasValue()) this.attribute('x', true).value = 0; + if (!this.attribute('y').hasValue()) this.attribute('y', true).value = 0; + ctx.translate(this.attribute('x').toPixels('x'), this.attribute('y').toPixels('y')); + + var width = svg.ViewPort.width(); + var height = svg.ViewPort.height(); + + if (!this.attribute('width').hasValue()) this.attribute('width', true).value = '100%'; + if (!this.attribute('height').hasValue()) this.attribute('height', true).value = '100%'; + if (typeof this.root === 'undefined') { + width = this.attribute('width').toPixels('x'); + height = this.attribute('height').toPixels('y'); + + var x = 0; + var y = 0; + if (this.attribute('refX').hasValue() && this.attribute('refY').hasValue()) { + x = -this.attribute('refX').toPixels('x'); + y = -this.attribute('refY').toPixels('y'); + } + + if (this.attribute('overflow').valueOrDefault('hidden') !== 'visible') { + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(width, y); + ctx.lineTo(width, height); + ctx.lineTo(x, height); + ctx.closePath(); + ctx.clip(); + } + } + svg.ViewPort.SetCurrent(width, height); + + // viewbox + if (this.attribute('viewBox').hasValue()) { + var viewBox = svg.ToNumberArray(this.attribute('viewBox').value); + var minX = viewBox[0]; + var minY = viewBox[1]; + width = viewBox[2]; + height = viewBox[3]; + + svg.AspectRatio(ctx, this.attribute('preserveAspectRatio').value, svg.ViewPort.width(), width, svg.ViewPort.height(), height, minX, minY, this.attribute('refX').value, this.attribute('refY').value); + + svg.ViewPort.RemoveCurrent(); + svg.ViewPort.SetCurrent(viewBox[2], viewBox[3]); + } + } + }]); + return _class7; + }(svg.Element.RenderedElementBase); + + // rect element + svg.Element.rect = function (_svg$Element$PathElem) { + inherits(_class8, _svg$Element$PathElem); + + function _class8() { + classCallCheck(this, _class8); + return possibleConstructorReturn(this, (_class8.__proto__ || Object.getPrototypeOf(_class8)).apply(this, arguments)); + } + + createClass(_class8, [{ + key: 'path', + value: function path(ctx) { + var x = this.attribute('x').toPixels('x'); + var y = this.attribute('y').toPixels('y'); + var width = this.attribute('width').toPixels('x'); + var height = this.attribute('height').toPixels('y'); + var rx = this.attribute('rx').toPixels('x'); + var ry = this.attribute('ry').toPixels('y'); + if (this.attribute('rx').hasValue() && !this.attribute('ry').hasValue()) ry = rx; + if (this.attribute('ry').hasValue() && !this.attribute('rx').hasValue()) rx = ry; + rx = Math.min(rx, width / 2.0); + ry = Math.min(ry, height / 2.0); + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(x + rx, y); + ctx.lineTo(x + width - rx, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + ry); + ctx.lineTo(x + width, y + height - ry); + ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height); + ctx.lineTo(x + rx, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - ry); + ctx.lineTo(x, y + ry); + ctx.quadraticCurveTo(x, y, x + rx, y); + ctx.closePath(); + } + + return new svg.BoundingBox(x, y, x + width, y + height); + } + }]); + return _class8; + }(svg.Element.PathElementBase); + + // circle element + svg.Element.circle = function (_svg$Element$PathElem2) { + inherits(_class9, _svg$Element$PathElem2); + + function _class9() { + classCallCheck(this, _class9); + return possibleConstructorReturn(this, (_class9.__proto__ || Object.getPrototypeOf(_class9)).apply(this, arguments)); + } + + createClass(_class9, [{ + key: 'path', + value: function path(ctx) { + var cx = this.attribute('cx').toPixels('x'); + var cy = this.attribute('cy').toPixels('y'); + var r = this.attribute('r').toPixels(); + + if (ctx != null) { + ctx.beginPath(); + ctx.arc(cx, cy, r, 0, Math.PI * 2, true); + ctx.closePath(); + } + + return new svg.BoundingBox(cx - r, cy - r, cx + r, cy + r); + } + }]); + return _class9; + }(svg.Element.PathElementBase); + + // ellipse element + var KAPPA = 4 * ((Math.sqrt(2) - 1) / 3); + svg.Element.ellipse = function (_svg$Element$PathElem3) { + inherits(_class10, _svg$Element$PathElem3); + + function _class10() { + classCallCheck(this, _class10); + return possibleConstructorReturn(this, (_class10.__proto__ || Object.getPrototypeOf(_class10)).apply(this, arguments)); + } + + createClass(_class10, [{ + key: 'path', + value: function path(ctx) { + var rx = this.attribute('rx').toPixels('x'); + var ry = this.attribute('ry').toPixels('y'); + var cx = this.attribute('cx').toPixels('x'); + var cy = this.attribute('cy').toPixels('y'); + + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(cx, cy - ry); + ctx.bezierCurveTo(cx + KAPPA * rx, cy - ry, cx + rx, cy - KAPPA * ry, cx + rx, cy); + ctx.bezierCurveTo(cx + rx, cy + KAPPA * ry, cx + KAPPA * rx, cy + ry, cx, cy + ry); + ctx.bezierCurveTo(cx - KAPPA * rx, cy + ry, cx - rx, cy + KAPPA * ry, cx - rx, cy); + ctx.bezierCurveTo(cx - rx, cy - KAPPA * ry, cx - KAPPA * rx, cy - ry, cx, cy - ry); + ctx.closePath(); + } + + return new svg.BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry); + } + }]); + return _class10; + }(svg.Element.PathElementBase); + + // line element + svg.Element.line = function (_svg$Element$PathElem4) { + inherits(_class11, _svg$Element$PathElem4); + + function _class11() { + classCallCheck(this, _class11); + return possibleConstructorReturn(this, (_class11.__proto__ || Object.getPrototypeOf(_class11)).apply(this, arguments)); + } + + createClass(_class11, [{ + key: 'getPoints', + value: function getPoints() { + return [new svg.Point(this.attribute('x1').toPixels('x'), this.attribute('y1').toPixels('y')), new svg.Point(this.attribute('x2').toPixels('x'), this.attribute('y2').toPixels('y'))]; + } + }, { + key: 'path', + value: function path(ctx) { + var points = this.getPoints(); + + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + ctx.lineTo(points[1].x, points[1].y); + } + + return new svg.BoundingBox(points[0].x, points[0].y, points[1].x, points[1].y); + } + }, { + key: 'getMarkers', + value: function getMarkers() { + var points = this.getPoints(); + var a = points[0].angleTo(points[1]); + return [[points[0], a], [points[1], a]]; + } + }]); + return _class11; + }(svg.Element.PathElementBase); + + // polyline element + svg.Element.polyline = function (_svg$Element$PathElem5) { + inherits(_class12, _svg$Element$PathElem5); + + function _class12(node) { + classCallCheck(this, _class12); + + var _this15 = possibleConstructorReturn(this, (_class12.__proto__ || Object.getPrototypeOf(_class12)).call(this, node)); + + _this15.points = svg.CreatePath(_this15.attribute('points').value); + return _this15; + } + + createClass(_class12, [{ + key: 'path', + value: function path(ctx) { + var _points$ = this.points[0], + x = _points$.x, + y = _points$.y; + + var bb = new svg.BoundingBox(x, y); + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(x, y); + } + for (var i = 1; i < this.points.length; i++) { + var _points$i = this.points[i], + _x = _points$i.x, + _y = _points$i.y; + + bb.addPoint(_x, _y); + if (ctx != null) ctx.lineTo(_x, _y); + } + return bb; + } + }, { + key: 'getMarkers', + value: function getMarkers() { + var markers = []; + for (var i = 0; i < this.points.length - 1; i++) { + markers.push([this.points[i], this.points[i].angleTo(this.points[i + 1])]); + } + markers.push([this.points[this.points.length - 1], markers[markers.length - 1][1]]); + return markers; + } + }]); + return _class12; + }(svg.Element.PathElementBase); + + // polygon element + svg.Element.polygon = function (_svg$Element$polyline) { + inherits(_class13, _svg$Element$polyline); + + function _class13() { + classCallCheck(this, _class13); + return possibleConstructorReturn(this, (_class13.__proto__ || Object.getPrototypeOf(_class13)).apply(this, arguments)); + } + + createClass(_class13, [{ + key: 'path', + value: function path(ctx) { + var bb = get(_class13.prototype.__proto__ || Object.getPrototypeOf(_class13.prototype), 'path', this).call(this, ctx); + if (ctx != null) { + ctx.lineTo(this.points[0].x, this.points[0].y); + ctx.closePath(); + } + return bb; + } + }]); + return _class13; + }(svg.Element.polyline); + + // path element + svg.Element.path = function (_svg$Element$PathElem6) { + inherits(_class14, _svg$Element$PathElem6); + + function _class14(node) { + classCallCheck(this, _class14); + + var _this17 = possibleConstructorReturn(this, (_class14.__proto__ || Object.getPrototypeOf(_class14)).call(this, node)); + + var d = _this17.attribute('d').value + // TODO: convert to real lexer based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF + .replace(/,/gm, ' ') // get rid of all commas + .replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from commands + .replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from commands + .replace(/([MmZzLlHhVvCcSsQqTtAa])([^\s])/gm, '$1 $2') // separate commands from points + .replace(/([^\s])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from points + .replace(/([0-9])([+-])/gm, '$1 $2') // separate digits when no comma + .replace(/(\.[0-9]*)(\.)/gm, '$1 $2') // separate digits when no comma + .replace(/([Aa](\s+[0-9]+){3})\s+([01])\s*([01])/gm, '$1 $3 $4 '); // shorthand elliptical arc path syntax + d = svg.compressSpaces(d); // compress multiple spaces + d = svg.trim(d); + _this17.PathParser = { + tokens: d.split(' '), + + reset: function reset() { + this.i = -1; + this.command = ''; + this.previousCommand = ''; + this.start = new svg.Point(0, 0); + this.control = new svg.Point(0, 0); + this.current = new svg.Point(0, 0); + this.points = []; + this.angles = []; + }, + isEnd: function isEnd() { + return this.i >= this.tokens.length - 1; + }, + isCommandOrEnd: function isCommandOrEnd() { + if (this.isEnd()) return true; + return this.tokens[this.i + 1].match(/^[A-Za-z]$/) != null; + }, + isRelativeCommand: function isRelativeCommand() { + switch (this.command) { + case 'm': + case 'l': + case 'h': + case 'v': + case 'c': + case 's': + case 'q': + case 't': + case 'a': + case 'z': + return true; + } + return false; + }, + getToken: function getToken() { + this.i++; + return this.tokens[this.i]; + }, + getScalar: function getScalar() { + return parseFloat(this.getToken()); + }, + nextCommand: function nextCommand() { + this.previousCommand = this.command; + this.command = this.getToken(); + }, + getPoint: function getPoint() { + var p = new svg.Point(this.getScalar(), this.getScalar()); + return this.makeAbsolute(p); + }, + getAsControlPoint: function getAsControlPoint() { + var p = this.getPoint(); + this.control = p; + return p; + }, + getAsCurrentPoint: function getAsCurrentPoint() { + var p = this.getPoint(); + this.current = p; + return p; + }, + getReflectedControlPoint: function getReflectedControlPoint() { + if (this.previousCommand.toLowerCase() !== 'c' && this.previousCommand.toLowerCase() !== 's' && this.previousCommand.toLowerCase() !== 'q' && this.previousCommand.toLowerCase() !== 't') { + return this.current; + } + + // reflect point + var p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y); + return p; + }, + makeAbsolute: function makeAbsolute(p) { + if (this.isRelativeCommand()) { + p.x += this.current.x; + p.y += this.current.y; + } + return p; + }, + addMarker: function addMarker(p, from, priorTo) { + // if the last angle isn't filled in because we didn't have this point yet ... + if (priorTo != null && this.angles.length > 0 && this.angles[this.angles.length - 1] == null) { + this.angles[this.angles.length - 1] = this.points[this.points.length - 1].angleTo(priorTo); + } + this.addMarkerAngle(p, from == null ? null : from.angleTo(p)); + }, + addMarkerAngle: function addMarkerAngle(p, a) { + this.points.push(p); + this.angles.push(a); + }, + getMarkerPoints: function getMarkerPoints() { + return this.points; + }, + getMarkerAngles: function getMarkerAngles() { + for (var i = 0; i < this.angles.length; i++) { + if (this.angles[i] == null) { + for (var j = i + 1; j < this.angles.length; j++) { + if (this.angles[j] != null) { + this.angles[i] = this.angles[j]; + break; + } + } + } + } + return this.angles; + } + }; + return _this17; + } + + createClass(_class14, [{ + key: 'path', + value: function path(ctx) { + var pp = this.PathParser; + pp.reset(); + + var bb = new svg.BoundingBox(); + if (ctx != null) ctx.beginPath(); + while (!pp.isEnd()) { + pp.nextCommand(); + switch (pp.command) { + case 'M': + case 'm': + var p = pp.getAsCurrentPoint(); + pp.addMarker(p); + bb.addPoint(p.x, p.y); + if (ctx != null) ctx.moveTo(p.x, p.y); + pp.start = pp.current; + while (!pp.isCommandOrEnd()) { + var _p = pp.getAsCurrentPoint(); + pp.addMarker(_p, pp.start); + bb.addPoint(_p.x, _p.y); + if (ctx != null) ctx.lineTo(_p.x, _p.y); + } + break; + case 'L': + case 'l': + while (!pp.isCommandOrEnd()) { + var _c = pp.current; + var _p2 = pp.getAsCurrentPoint(); + pp.addMarker(_p2, _c); + bb.addPoint(_p2.x, _p2.y); + if (ctx != null) ctx.lineTo(_p2.x, _p2.y); + } + break; + case 'H': + case 'h': + while (!pp.isCommandOrEnd()) { + var newP = new svg.Point((pp.isRelativeCommand() ? pp.current.x : 0) + pp.getScalar(), pp.current.y); + pp.addMarker(newP, pp.current); + pp.current = newP; + bb.addPoint(pp.current.x, pp.current.y); + if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y); + } + break; + case 'V': + case 'v': + while (!pp.isCommandOrEnd()) { + var _newP = new svg.Point(pp.current.x, (pp.isRelativeCommand() ? pp.current.y : 0) + pp.getScalar()); + pp.addMarker(_newP, pp.current); + pp.current = _newP; + bb.addPoint(pp.current.x, pp.current.y); + if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y); + } + break; + case 'C': + case 'c': + while (!pp.isCommandOrEnd()) { + var curr = pp.current; + var p1 = pp.getPoint(); + var cntrl = pp.getAsControlPoint(); + var cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, p1); + bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'S': + case 's': + while (!pp.isCommandOrEnd()) { + var _curr = pp.current; + var _p3 = pp.getReflectedControlPoint(); + var _cntrl = pp.getAsControlPoint(); + var _cp = pp.getAsCurrentPoint(); + pp.addMarker(_cp, _cntrl, _p3); + bb.addBezierCurve(_curr.x, _curr.y, _p3.x, _p3.y, _cntrl.x, _cntrl.y, _cp.x, _cp.y); + if (ctx != null) ctx.bezierCurveTo(_p3.x, _p3.y, _cntrl.x, _cntrl.y, _cp.x, _cp.y); + } + break; + case 'Q': + case 'q': + while (!pp.isCommandOrEnd()) { + var _curr2 = pp.current; + var _cntrl2 = pp.getAsControlPoint(); + var _cp2 = pp.getAsCurrentPoint(); + pp.addMarker(_cp2, _cntrl2, _cntrl2); + bb.addQuadraticCurve(_curr2.x, _curr2.y, _cntrl2.x, _cntrl2.y, _cp2.x, _cp2.y); + if (ctx != null) ctx.quadraticCurveTo(_cntrl2.x, _cntrl2.y, _cp2.x, _cp2.y); + } + break; + case 'T': + case 't': + while (!pp.isCommandOrEnd()) { + var _curr3 = pp.current; + var _cntrl3 = pp.getReflectedControlPoint(); + pp.control = _cntrl3; + var _cp3 = pp.getAsCurrentPoint(); + pp.addMarker(_cp3, _cntrl3, _cntrl3); + bb.addQuadraticCurve(_curr3.x, _curr3.y, _cntrl3.x, _cntrl3.y, _cp3.x, _cp3.y); + if (ctx != null) ctx.quadraticCurveTo(_cntrl3.x, _cntrl3.y, _cp3.x, _cp3.y); + } + break; + case 'A': + case 'a': + var _loop2 = function _loop2() { + var curr = pp.current; + var rx = pp.getScalar(); + var ry = pp.getScalar(); + var xAxisRotation = pp.getScalar() * (Math.PI / 180.0); + var largeArcFlag = pp.getScalar(); + var sweepFlag = pp.getScalar(); + var cp = pp.getAsCurrentPoint(); + + // Conversion from endpoint to center parameterization + // https://www.w3.org/TR/SVG11/implnote.html#ArcConversionEndpointToCenter + + // x1', y1' + var currp = new svg.Point(Math.cos(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.sin(xAxisRotation) * (curr.y - cp.y) / 2.0, -Math.sin(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.cos(xAxisRotation) * (curr.y - cp.y) / 2.0); + // adjust radii + var l = Math.pow(currp.x, 2) / Math.pow(rx, 2) + Math.pow(currp.y, 2) / Math.pow(ry, 2); + if (l > 1) { + rx *= Math.sqrt(l); + ry *= Math.sqrt(l); + } + // cx', cy' + var s = (largeArcFlag === sweepFlag ? -1 : 1) * Math.sqrt((Math.pow(rx, 2) * Math.pow(ry, 2) - Math.pow(rx, 2) * Math.pow(currp.y, 2) - Math.pow(ry, 2) * Math.pow(currp.x, 2)) / (Math.pow(rx, 2) * Math.pow(currp.y, 2) + Math.pow(ry, 2) * Math.pow(currp.x, 2))); + if (isNaN(s)) s = 0; + var cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); + // cx, cy + var centp = new svg.Point((curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y); + // vector magnitude + var m = function m(v) { + return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2)); + }; + // ratio between two vectors + var r = function r(u, v) { + return (u[0] * v[0] + u[1] * v[1]) / (m(u) * m(v)); + }; + // angle between two vectors + var a = function a(u, v) { + return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(r(u, v)); + }; + // initial angle + var a1 = a([1, 0], [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]); + // angle delta + var u = [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]; + var v = [(-currp.x - cpp.x) / rx, (-currp.y - cpp.y) / ry]; + var ad = a(u, v); + if (r(u, v) <= -1) ad = Math.PI; + if (r(u, v) >= 1) ad = 0; + + // for markers + var dir = 1 - sweepFlag ? 1.0 : -1.0; + var ah = a1 + dir * (ad / 2.0); + var halfWay = new svg.Point(centp.x + rx * Math.cos(ah), centp.y + ry * Math.sin(ah)); + pp.addMarkerAngle(halfWay, ah - dir * Math.PI / 2); + pp.addMarkerAngle(cp, ah - dir * Math.PI); + + bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better + if (ctx != null) { + var _r = rx > ry ? rx : ry; + var sx = rx > ry ? 1 : rx / ry; + var sy = rx > ry ? ry / rx : 1; + + ctx.translate(centp.x, centp.y); + ctx.rotate(xAxisRotation); + ctx.scale(sx, sy); + ctx.arc(0, 0, _r, a1, a1 + ad, 1 - sweepFlag); + ctx.scale(1 / sx, 1 / sy); + ctx.rotate(-xAxisRotation); + ctx.translate(-centp.x, -centp.y); + } + }; + + while (!pp.isCommandOrEnd()) { + _loop2(); + } + break; + case 'Z': + case 'z': + if (ctx != null) ctx.closePath(); + pp.current = pp.start; + } + } + + return bb; + } + }, { + key: 'getMarkers', + value: function getMarkers() { + var points = this.PathParser.getMarkerPoints(); + var angles = this.PathParser.getMarkerAngles(); + + var markers = points.map(function (point, i) { + return [point, angles[i]]; + }); + return markers; + } + }]); + return _class14; + }(svg.Element.PathElementBase); + + // pattern element + svg.Element.pattern = function (_svg$Element$ElementB2) { + inherits(_class15, _svg$Element$ElementB2); + + function _class15() { + classCallCheck(this, _class15); + return possibleConstructorReturn(this, (_class15.__proto__ || Object.getPrototypeOf(_class15)).apply(this, arguments)); + } + + createClass(_class15, [{ + key: 'createPattern', + value: function createPattern(ctx, element) { + var width = this.attribute('width').toPixels('x', true); + var height = this.attribute('height').toPixels('y', true); + + // render me using a temporary svg element + var tempSvg = new svg.Element.svg(); + tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value); + tempSvg.attributes['width'] = new svg.Property('width', width + 'px'); + tempSvg.attributes['height'] = new svg.Property('height', height + 'px'); + tempSvg.attributes['transform'] = new svg.Property('transform', this.attribute('patternTransform').value); + tempSvg.children = this.children; + + var c = document.createElement('canvas'); + c.width = width; + c.height = height; + var cctx = c.getContext('2d'); + if (this.attribute('x').hasValue() && this.attribute('y').hasValue()) { + cctx.translate(this.attribute('x').toPixels('x', true), this.attribute('y').toPixels('y', true)); + } + // render 3x3 grid so when we transform there's no white space on edges + for (var x = -1; x <= 1; x++) { + for (var y = -1; y <= 1; y++) { + cctx.save(); + cctx.translate(x * c.width, y * c.height); + tempSvg.render(cctx); + cctx.restore(); + } + } + var pattern = ctx.createPattern(c, 'repeat'); + return pattern; + } + }]); + return _class15; + }(svg.Element.ElementBase); + + // marker element + svg.Element.marker = function (_svg$Element$ElementB3) { + inherits(_class16, _svg$Element$ElementB3); + + function _class16() { + classCallCheck(this, _class16); + return possibleConstructorReturn(this, (_class16.__proto__ || Object.getPrototypeOf(_class16)).apply(this, arguments)); + } + + createClass(_class16, [{ + key: 'render', + value: function render(ctx, point, angle) { + ctx.translate(point.x, point.y); + if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(angle); + if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(ctx.lineWidth, ctx.lineWidth); + ctx.save(); + + // render me using a temporary svg element + var tempSvg = new svg.Element.svg(); + tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value); + tempSvg.attributes['refX'] = new svg.Property('refX', this.attribute('refX').value); + tempSvg.attributes['refY'] = new svg.Property('refY', this.attribute('refY').value); + tempSvg.attributes['width'] = new svg.Property('width', this.attribute('markerWidth').value); + tempSvg.attributes['height'] = new svg.Property('height', this.attribute('markerHeight').value); + tempSvg.attributes['fill'] = new svg.Property('fill', this.attribute('fill').valueOrDefault('black')); + tempSvg.attributes['stroke'] = new svg.Property('stroke', this.attribute('stroke').valueOrDefault('none')); + tempSvg.children = this.children; + tempSvg.render(ctx); + + ctx.restore(); + if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(1 / ctx.lineWidth, 1 / ctx.lineWidth); + if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(-angle); + ctx.translate(-point.x, -point.y); + } + }]); + return _class16; + }(svg.Element.ElementBase); + + // definitions element + svg.Element.defs = function (_svg$Element$ElementB4) { + inherits(_class17, _svg$Element$ElementB4); + + function _class17() { + classCallCheck(this, _class17); + return possibleConstructorReturn(this, (_class17.__proto__ || Object.getPrototypeOf(_class17)).apply(this, arguments)); + } + + createClass(_class17, [{ + key: 'render', + value: function render(ctx) { + // NOOP + } + }]); + return _class17; + }(svg.Element.ElementBase); + + // base for gradients + svg.Element.GradientBase = function (_svg$Element$ElementB5) { + inherits(_class18, _svg$Element$ElementB5); + + function _class18(node) { + classCallCheck(this, _class18); + + var _this21 = possibleConstructorReturn(this, (_class18.__proto__ || Object.getPrototypeOf(_class18)).call(this, node)); + + _this21.gradientUnits = _this21.attribute('gradientUnits').valueOrDefault('objectBoundingBox'); + + _this21.stops = []; + _this21.children.forEach(function (child) { + if (child.type === 'stop') { + _this21.stops.push(child); + } + }); + return _this21; + } + + createClass(_class18, [{ + key: 'getGradient', + value: function getGradient() { + // OVERRIDE ME! + } + }, { + key: 'createGradient', + value: function createGradient(ctx, element, parentOpacityProp) { + var stopsContainer = this.getHrefAttribute().hasValue() ? this.getHrefAttribute().getDefinition() : this; + + var addParentOpacity = function addParentOpacity(color) { + if (parentOpacityProp.hasValue()) { + var p = new svg.Property('color', color); + return p.addOpacity(parentOpacityProp).value; + } + return color; + }; + + var g = this.getGradient(ctx, element); + if (g == null) return addParentOpacity(stopsContainer.stops[stopsContainer.stops.length - 1].color); + stopsContainer.stops.forEach(function (_ref2) { + var offset = _ref2.offset, + color = _ref2.color; + + g.addColorStop(offset, addParentOpacity(color)); + }); + + if (this.attribute('gradientTransform').hasValue()) { + // render as transformed pattern on temporary canvas + var rootView = svg.ViewPort.viewPorts[0]; + + var rect = new svg.Element.rect(); + rect.attributes['x'] = new svg.Property('x', -svg.MAX_VIRTUAL_PIXELS / 3.0); + rect.attributes['y'] = new svg.Property('y', -svg.MAX_VIRTUAL_PIXELS / 3.0); + rect.attributes['width'] = new svg.Property('width', svg.MAX_VIRTUAL_PIXELS); + rect.attributes['height'] = new svg.Property('height', svg.MAX_VIRTUAL_PIXELS); + + var group = new svg.Element.g(); + group.attributes['transform'] = new svg.Property('transform', this.attribute('gradientTransform').value); + group.children = [rect]; + + var tempSvg = new svg.Element.svg(); + tempSvg.attributes['x'] = new svg.Property('x', 0); + tempSvg.attributes['y'] = new svg.Property('y', 0); + tempSvg.attributes['width'] = new svg.Property('width', rootView.width); + tempSvg.attributes['height'] = new svg.Property('height', rootView.height); + tempSvg.children = [group]; + + var _c2 = document.createElement('canvas'); + _c2.width = rootView.width; + _c2.height = rootView.height; + var tempCtx = _c2.getContext('2d'); + tempCtx.fillStyle = g; + tempSvg.render(tempCtx); + return tempCtx.createPattern(_c2, 'no-repeat'); + } + + return g; + } + }]); + return _class18; + }(svg.Element.ElementBase); + + // linear gradient element + svg.Element.linearGradient = function (_svg$Element$Gradient) { + inherits(_class19, _svg$Element$Gradient); + + function _class19() { + classCallCheck(this, _class19); + return possibleConstructorReturn(this, (_class19.__proto__ || Object.getPrototypeOf(_class19)).apply(this, arguments)); + } + + createClass(_class19, [{ + key: 'getGradient', + value: function getGradient(ctx, element) { + var useBB = this.gradientUnits === 'objectBoundingBox' && element.getBoundingBox; + var bb = useBB ? element.getBoundingBox() : null; + + if (!this.attribute('x1').hasValue() && !this.attribute('y1').hasValue() && !this.attribute('x2').hasValue() && !this.attribute('y2').hasValue()) { + this.attribute('x1', true).value = 0; + this.attribute('y1', true).value = 0; + this.attribute('x2', true).value = 1; + this.attribute('y2', true).value = 0; + } + + var x1 = useBB ? bb.x() + bb.width() * this.attribute('x1').numValue() : this.attribute('x1').toPixels('x'); + var y1 = useBB ? bb.y() + bb.height() * this.attribute('y1').numValue() : this.attribute('y1').toPixels('y'); + var x2 = useBB ? bb.x() + bb.width() * this.attribute('x2').numValue() : this.attribute('x2').toPixels('x'); + var y2 = useBB ? bb.y() + bb.height() * this.attribute('y2').numValue() : this.attribute('y2').toPixels('y'); + + if (x1 === x2 && y1 === y2) return null; + return ctx.createLinearGradient(x1, y1, x2, y2); + } + }]); + return _class19; + }(svg.Element.GradientBase); + + // radial gradient element + svg.Element.radialGradient = function (_svg$Element$Gradient2) { + inherits(_class20, _svg$Element$Gradient2); + + function _class20() { + classCallCheck(this, _class20); + return possibleConstructorReturn(this, (_class20.__proto__ || Object.getPrototypeOf(_class20)).apply(this, arguments)); + } + + createClass(_class20, [{ + key: 'getGradient', + value: function getGradient(ctx, element) { + var useBB = this.gradientUnits === 'objectBoundingBox' && element.getBoundingBox; + var bb = useBB ? element.getBoundingBox() : null; + + if (!this.attribute('cx').hasValue()) this.attribute('cx', true).value = '50%'; + if (!this.attribute('cy').hasValue()) this.attribute('cy', true).value = '50%'; + if (!this.attribute('r').hasValue()) this.attribute('r', true).value = '50%'; + + var cx = useBB ? bb.x() + bb.width() * this.attribute('cx').numValue() : this.attribute('cx').toPixels('x'); + var cy = useBB ? bb.y() + bb.height() * this.attribute('cy').numValue() : this.attribute('cy').toPixels('y'); + + var fx = cx; + var fy = cy; + if (this.attribute('fx').hasValue()) { + fx = useBB ? bb.x() + bb.width() * this.attribute('fx').numValue() : this.attribute('fx').toPixels('x'); + } + if (this.attribute('fy').hasValue()) { + fy = useBB ? bb.y() + bb.height() * this.attribute('fy').numValue() : this.attribute('fy').toPixels('y'); + } + + var r = useBB ? (bb.width() + bb.height()) / 2.0 * this.attribute('r').numValue() : this.attribute('r').toPixels(); + + return ctx.createRadialGradient(fx, fy, 0, cx, cy, r); + } + }]); + return _class20; + }(svg.Element.GradientBase); + + // gradient stop element + svg.Element.stop = function (_svg$Element$ElementB6) { + inherits(_class21, _svg$Element$ElementB6); + + function _class21(node) { + classCallCheck(this, _class21); + + var _this24 = possibleConstructorReturn(this, (_class21.__proto__ || Object.getPrototypeOf(_class21)).call(this, node)); + + _this24.offset = _this24.attribute('offset').numValue(); + if (_this24.offset < 0) _this24.offset = 0; + if (_this24.offset > 1) _this24.offset = 1; + + var stopColor = _this24.style('stop-color'); + if (_this24.style('stop-opacity').hasValue()) { + stopColor = stopColor.addOpacity(_this24.style('stop-opacity')); + } + _this24.color = stopColor.value; + return _this24; + } + + return _class21; + }(svg.Element.ElementBase); + + // animation base element + svg.Element.AnimateBase = function (_svg$Element$ElementB7) { + inherits(_class22, _svg$Element$ElementB7); + + function _class22(node) { + classCallCheck(this, _class22); + + var _this25 = possibleConstructorReturn(this, (_class22.__proto__ || Object.getPrototypeOf(_class22)).call(this, node)); + + svg.Animations.push(_this25); + + _this25.duration = 0.0; + _this25.begin = _this25.attribute('begin').toMilliseconds(); + _this25.maxDuration = _this25.begin + _this25.attribute('dur').toMilliseconds(); + + _this25.initialValue = null; + _this25.initialUnits = ''; + _this25.removed = false; + + _this25.from = _this25.attribute('from'); + _this25.to = _this25.attribute('to'); + _this25.values = _this25.attribute('values'); + if (_this25.values.hasValue()) _this25.values.value = _this25.values.value.split(';'); + return _this25; + } + + createClass(_class22, [{ + key: 'getProperty', + value: function getProperty() { + var attributeType = this.attribute('attributeType').value; + var attributeName = this.attribute('attributeName').value; + + if (attributeType === 'CSS') { + return this.parent.style(attributeName, true); + } + return this.parent.attribute(attributeName, true); + } + }, { + key: 'calcValue', + value: function calcValue() { + // OVERRIDE ME! + return ''; + } + }, { + key: 'update', + value: function update(delta) { + // set initial value + if (this.initialValue == null) { + this.initialValue = this.getProperty().value; + this.initialUnits = this.getProperty().getUnits(); + } + + // if we're past the end time + if (this.duration > this.maxDuration) { + // loop for indefinitely repeating animations + if (this.attribute('repeatCount').value === 'indefinite' || this.attribute('repeatDur').value === 'indefinite') { + this.duration = 0.0; + } else if (this.attribute('fill').valueOrDefault('remove') === 'freeze' && !this.frozen) { + this.frozen = true; + this.parent.animationFrozen = true; + this.parent.animationFrozenValue = this.getProperty().value; + } else if (this.attribute('fill').valueOrDefault('remove') === 'remove' && !this.removed) { + this.removed = true; + this.getProperty().value = this.parent.animationFrozen ? this.parent.animationFrozenValue : this.initialValue; + return true; + } + return false; + } + this.duration = this.duration + delta; + + // if we're past the begin time + var updated = false; + if (this.begin < this.duration) { + var newValue = this.calcValue(); // tween + + if (this.attribute('type').hasValue()) { + // for transform, etc. + var type = this.attribute('type').value; + newValue = type + '(' + newValue + ')'; + } + + this.getProperty().value = newValue; + updated = true; + } + + return updated; + } + + // fraction of duration we've covered + + }, { + key: 'progress', + value: function progress() { + var ret = { progress: (this.duration - this.begin) / (this.maxDuration - this.begin) }; + if (this.values.hasValue()) { + var p = ret.progress * (this.values.value.length - 1); + var lb = Math.floor(p), + ub = Math.ceil(p); + ret.from = new svg.Property('from', parseFloat(this.values.value[lb])); + ret.to = new svg.Property('to', parseFloat(this.values.value[ub])); + ret.progress = (p - lb) / (ub - lb); + } else { + ret.from = this.from; + ret.to = this.to; + } + return ret; + } + }]); + return _class22; + }(svg.Element.ElementBase); + + // animate element + svg.Element.animate = function (_svg$Element$AnimateB) { + inherits(_class23, _svg$Element$AnimateB); + + function _class23() { + classCallCheck(this, _class23); + return possibleConstructorReturn(this, (_class23.__proto__ || Object.getPrototypeOf(_class23)).apply(this, arguments)); + } + + createClass(_class23, [{ + key: 'calcValue', + value: function calcValue() { + var p = this.progress(); + + // tween value linearly + var newValue = p.from.numValue() + (p.to.numValue() - p.from.numValue()) * p.progress; + return newValue + this.initialUnits; + } + }]); + return _class23; + }(svg.Element.AnimateBase); + + // animate color element + svg.Element.animateColor = function (_svg$Element$AnimateB2) { + inherits(_class24, _svg$Element$AnimateB2); + + function _class24() { + classCallCheck(this, _class24); + return possibleConstructorReturn(this, (_class24.__proto__ || Object.getPrototypeOf(_class24)).apply(this, arguments)); + } + + createClass(_class24, [{ + key: 'calcValue', + value: function calcValue() { + var p = this.progress(); + var from = new RGBColor(p.from.value); + var to = new RGBColor(p.to.value); + + if (from.ok && to.ok) { + // tween color linearly + var _r2 = from.r + (to.r - from.r) * p.progress; + var g = from.g + (to.g - from.g) * p.progress; + var _b = from.b + (to.b - from.b) * p.progress; + return 'rgb(' + parseInt(_r2, 10) + ',' + parseInt(g, 10) + ',' + parseInt(_b, 10) + ')'; + } + return this.attribute('from').value; + } + }]); + return _class24; + }(svg.Element.AnimateBase); + + // animate transform element + svg.Element.animateTransform = function (_svg$Element$animate) { + inherits(_class25, _svg$Element$animate); + + function _class25() { + classCallCheck(this, _class25); + return possibleConstructorReturn(this, (_class25.__proto__ || Object.getPrototypeOf(_class25)).apply(this, arguments)); + } + + createClass(_class25, [{ + key: 'calcValue', + value: function calcValue() { + var p = this.progress(); + + // tween value linearly + var from = svg.ToNumberArray(p.from.value); + var to = svg.ToNumberArray(p.to.value); + var newValue = ''; + from.forEach(function (fr, i) { + newValue += fr + (to[i] - fr) * p.progress + ' '; + }); + return newValue; + } + }]); + return _class25; + }(svg.Element.animate); + + // font element + svg.Element.font = function (_svg$Element$ElementB8) { + inherits(_class26, _svg$Element$ElementB8); + + function _class26(node) { + classCallCheck(this, _class26); + + var _this29 = possibleConstructorReturn(this, (_class26.__proto__ || Object.getPrototypeOf(_class26)).call(this, node)); + + _this29.horizAdvX = _this29.attribute('horiz-adv-x').numValue(); + + _this29.isRTL = false; + _this29.isArabic = false; + _this29.fontFace = null; + _this29.missingGlyph = null; + _this29.glyphs = []; + _this29.children.forEach(function (child) { + if (child.type === 'font-face') { + _this29.fontFace = child; + if (child.style('font-family').hasValue()) { + svg.Definitions[child.style('font-family').value] = _this29; + } + } else if (child.type === 'missing-glyph') { + _this29.missingGlyph = child; + } else if (child.type === 'glyph') { + if (child.arabicForm !== '') { + _this29.isRTL = true; + _this29.isArabic = true; + if (typeof _this29.glyphs[child.unicode] === 'undefined') { + _this29.glyphs[child.unicode] = []; + } + _this29.glyphs[child.unicode][child.arabicForm] = child; + } else { + _this29.glyphs[child.unicode] = child; + } + } + }); + return _this29; + } + + return _class26; + }(svg.Element.ElementBase); + + // font-face element + svg.Element.fontface = function (_svg$Element$ElementB9) { + inherits(_class27, _svg$Element$ElementB9); + + function _class27(node) { + classCallCheck(this, _class27); + + var _this30 = possibleConstructorReturn(this, (_class27.__proto__ || Object.getPrototypeOf(_class27)).call(this, node)); + + _this30.ascent = _this30.attribute('ascent').value; + _this30.descent = _this30.attribute('descent').value; + _this30.unitsPerEm = _this30.attribute('units-per-em').numValue(); + return _this30; + } + + return _class27; + }(svg.Element.ElementBase); + + // missing-glyph element + svg.Element.missingglyph = function (_svg$Element$path) { + inherits(_class28, _svg$Element$path); + + function _class28(node) { + classCallCheck(this, _class28); + + var _this31 = possibleConstructorReturn(this, (_class28.__proto__ || Object.getPrototypeOf(_class28)).call(this, node)); + + _this31.horizAdvX = 0; + return _this31; + } + + return _class28; + }(svg.Element.path); + + // glyph element + svg.Element.glyph = function (_svg$Element$path2) { + inherits(_class29, _svg$Element$path2); + + function _class29(node) { + classCallCheck(this, _class29); + + var _this32 = possibleConstructorReturn(this, (_class29.__proto__ || Object.getPrototypeOf(_class29)).call(this, node)); + + _this32.horizAdvX = _this32.attribute('horiz-adv-x').numValue(); + _this32.unicode = _this32.attribute('unicode').value; + _this32.arabicForm = _this32.attribute('arabic-form').value; + return _this32; + } + + return _class29; + }(svg.Element.path); + + // text element + svg.Element.text = function (_svg$Element$Rendered3) { + inherits(_class30, _svg$Element$Rendered3); + + function _class30(node) { + classCallCheck(this, _class30); + return possibleConstructorReturn(this, (_class30.__proto__ || Object.getPrototypeOf(_class30)).call(this, node, true)); + } + + createClass(_class30, [{ + key: 'setContext', + value: function setContext(ctx) { + get(_class30.prototype.__proto__ || Object.getPrototypeOf(_class30.prototype), 'setContext', this).call(this, ctx); + + var textBaseline = this.style('dominant-baseline').toTextBaseline(); + if (textBaseline == null) textBaseline = this.style('alignment-baseline').toTextBaseline(); + if (textBaseline != null) ctx.textBaseline = textBaseline; + } + }, { + key: 'getBoundingBox', + value: function getBoundingBox() { + var x = this.attribute('x').toPixels('x'); + var y = this.attribute('y').toPixels('y'); + var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + return new svg.BoundingBox(x, y - fontSize, x + Math.floor(fontSize * 2.0 / 3.0) * this.children[0].getText().length, y); + } + }, { + key: 'renderChildren', + value: function renderChildren(ctx) { + var _this34 = this; + + this.x = this.attribute('x').toPixels('x'); + this.y = this.attribute('y').toPixels('y'); + this.x += this.getAnchorDelta(ctx, this, 0); + this.children.forEach(function (child, i) { + _this34.renderChild(ctx, _this34, i); + }); + } + }, { + key: 'getAnchorDelta', + value: function getAnchorDelta(ctx, parent, startI) { + var textAnchor = this.style('text-anchor').valueOrDefault('start'); + if (textAnchor !== 'start') { + var width = 0; + for (var i = startI; i < parent.children.length; i++) { + var child = parent.children[i]; + if (i > startI && child.attribute('x').hasValue()) break; // new group + width += child.measureTextRecursive(ctx); + } + return -1 * (textAnchor === 'end' ? width : width / 2.0); + } + return 0; + } + }, { + key: 'renderChild', + value: function renderChild(ctx, parent, i) { + var child = parent.children[i]; + if (child.attribute('x').hasValue()) { + child.x = child.attribute('x').toPixels('x') + this.getAnchorDelta(ctx, parent, i); + if (child.attribute('dx').hasValue()) child.x += child.attribute('dx').toPixels('x'); + } else { + if (this.attribute('dx').hasValue()) this.x += this.attribute('dx').toPixels('x'); + if (child.attribute('dx').hasValue()) this.x += child.attribute('dx').toPixels('x'); + child.x = this.x; + } + this.x = child.x + child.measureText(ctx); + + if (child.attribute('y').hasValue()) { + child.y = child.attribute('y').toPixels('y'); + if (child.attribute('dy').hasValue()) child.y += child.attribute('dy').toPixels('y'); + } else { + if (this.attribute('dy').hasValue()) this.y += this.attribute('dy').toPixels('y'); + if (child.attribute('dy').hasValue()) this.y += child.attribute('dy').toPixels('y'); + child.y = this.y; + } + this.y = child.y; + + child.render(ctx); + + for (var _i = 0; _i < child.children.length; _i++) { + this.renderChild(ctx, child, _i); + } + } + }]); + return _class30; + }(svg.Element.RenderedElementBase); + + // text base + svg.Element.TextElementBase = function (_svg$Element$Rendered4) { + inherits(_class31, _svg$Element$Rendered4); + + function _class31() { + classCallCheck(this, _class31); + return possibleConstructorReturn(this, (_class31.__proto__ || Object.getPrototypeOf(_class31)).apply(this, arguments)); + } + + createClass(_class31, [{ + key: 'getGlyph', + value: function getGlyph(font, text, i) { + var c = text[i]; + var glyph = null; + if (font.isArabic) { + var arabicForm = 'isolated'; + if ((i === 0 || text[i - 1] === ' ') && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'terminal'; + if (i > 0 && text[i - 1] !== ' ' && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'medial'; + if (i > 0 && text[i - 1] !== ' ' && (i === text.length - 1 || text[i + 1] === ' ')) arabicForm = 'initial'; + if (typeof font.glyphs[c] !== 'undefined') { + glyph = font.glyphs[c][arabicForm]; + if (glyph == null && font.glyphs[c].type === 'glyph') glyph = font.glyphs[c]; + } + } else { + glyph = font.glyphs[c]; + } + if (glyph == null) glyph = font.missingGlyph; + return glyph; + } + }, { + key: 'renderChildren', + value: function renderChildren(ctx) { + var customFont = this.parent.style('font-family').getDefinition(); + if (customFont != null) { + var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + var fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle); + var text = this.getText(); + if (customFont.isRTL) text = text.split('').reverse().join(''); + + var dx = svg.ToNumberArray(this.parent.attribute('dx').value); + for (var i = 0; i < text.length; i++) { + var glyph = this.getGlyph(customFont, text, i); + var scale = fontSize / customFont.fontFace.unitsPerEm; + ctx.translate(this.x, this.y); + ctx.scale(scale, -scale); + var lw = ctx.lineWidth; + ctx.lineWidth = ctx.lineWidth * customFont.fontFace.unitsPerEm / fontSize; + if (fontStyle === 'italic') ctx.transform(1, 0, 0.4, 1, 0, 0); + glyph.render(ctx); + if (fontStyle === 'italic') ctx.transform(1, 0, -0.4, 1, 0, 0); + ctx.lineWidth = lw; + ctx.scale(1 / scale, -1 / scale); + ctx.translate(-this.x, -this.y); + + this.x += fontSize * (glyph.horizAdvX || customFont.horizAdvX) / customFont.fontFace.unitsPerEm; + if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { + this.x += dx[i]; + } + } + return; + } + + if (ctx.fillStyle !== '') ctx.fillText(svg.compressSpaces(this.getText()), this.x, this.y); + if (ctx.strokeStyle !== '') ctx.strokeText(svg.compressSpaces(this.getText()), this.x, this.y); + } + }, { + key: 'getText', + value: function getText() { + // OVERRIDE ME + } + }, { + key: 'measureTextRecursive', + value: function measureTextRecursive(ctx) { + var width = this.measureText(ctx); + this.children.forEach(function (child) { + width += child.measureTextRecursive(ctx); + }); + return width; + } + }, { + key: 'measureText', + value: function measureText(ctx) { + var customFont = this.parent.style('font-family').getDefinition(); + if (customFont != null) { + var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + var measure = 0; + var text = this.getText(); + if (customFont.isRTL) text = text.split('').reverse().join(''); + var dx = svg.ToNumberArray(this.parent.attribute('dx').value); + for (var i = 0; i < text.length; i++) { + var glyph = this.getGlyph(customFont, text, i); + measure += (glyph.horizAdvX || customFont.horizAdvX) * fontSize / customFont.fontFace.unitsPerEm; + if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { + measure += dx[i]; + } + } + return measure; + } + + var textToMeasure = svg.compressSpaces(this.getText()); + if (!ctx.measureText) return textToMeasure.length * 10; + + ctx.save(); + this.setContext(ctx); + + var _ctx$measureText = ctx.measureText(textToMeasure), + width = _ctx$measureText.width; + + ctx.restore(); + return width; + } + }]); + return _class31; + }(svg.Element.RenderedElementBase); + + // tspan + svg.Element.tspan = function (_svg$Element$TextElem) { + inherits(_class32, _svg$Element$TextElem); + + function _class32(node) { + classCallCheck(this, _class32); + + var _this36 = possibleConstructorReturn(this, (_class32.__proto__ || Object.getPrototypeOf(_class32)).call(this, node, true)); + + _this36.text = node.nodeValue || node.text || ''; + return _this36; + } + + createClass(_class32, [{ + key: 'getText', + value: function getText() { + return this.text; + } + }]); + return _class32; + }(svg.Element.TextElementBase); + + // tref + svg.Element.tref = function (_svg$Element$TextElem2) { + inherits(_class33, _svg$Element$TextElem2); + + function _class33() { + classCallCheck(this, _class33); + return possibleConstructorReturn(this, (_class33.__proto__ || Object.getPrototypeOf(_class33)).apply(this, arguments)); + } + + createClass(_class33, [{ + key: 'getText', + value: function getText() { + var element = this.getHrefAttribute().getDefinition(); + if (element != null) return element.children[0].getText(); + } + }]); + return _class33; + }(svg.Element.TextElementBase); + + // a element + svg.Element.a = function (_svg$Element$TextElem3) { + inherits(_class34, _svg$Element$TextElem3); + + function _class34(node) { + classCallCheck(this, _class34); + + var _this38 = possibleConstructorReturn(this, (_class34.__proto__ || Object.getPrototypeOf(_class34)).call(this, node)); + + _this38.hasText = true; + [].concat(toConsumableArray(node.childNodes)).forEach(function (childNode) { + if (childNode.nodeType !== 3) { + _this38.hasText = false; + } + }); + // this might contain text + _this38.text = _this38.hasText ? node.childNodes[0].nodeValue : ''; + return _this38; + } + + createClass(_class34, [{ + key: 'getText', + value: function getText() { + return this.text; + } + }, { + key: 'renderChildren', + value: function renderChildren(ctx) { + if (this.hasText) { + // render as text element + get(_class34.prototype.__proto__ || Object.getPrototypeOf(_class34.prototype), 'renderChildren', this).call(this, ctx); + var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + svg.Mouse.checkBoundingBox(this, new svg.BoundingBox(this.x, this.y - fontSize.toPixels('y'), this.x + this.measureText(ctx), this.y)); + } else { + // render as temporary group + var g = new svg.Element.g(); + g.children = this.children; + g.parent = this; + g.render(ctx); + } + } + }, { + key: 'onclick', + value: function onclick() { + window.open(this.getHrefAttribute().value); + } + }, { + key: 'onmousemove', + value: function onmousemove() { + svg.ctx.canvas.style.cursor = 'pointer'; + } + }]); + return _class34; + }(svg.Element.TextElementBase); + + // image element + svg.Element.image = function (_svg$Element$Rendered5) { + inherits(_class35, _svg$Element$Rendered5); + + function _class35(node) { + classCallCheck(this, _class35); + + var _this39 = possibleConstructorReturn(this, (_class35.__proto__ || Object.getPrototypeOf(_class35)).call(this, node)); + + var href = _this39.getHrefAttribute().value; + if (href === '') { + return possibleConstructorReturn(_this39); + } + _this39._isSvg = href.match(/\.svg$/); + + svg.Images.push(_this39); + _this39.loaded = false; + if (!_this39._isSvg) { + _this39.img = document.createElement('img'); + if (svg.opts.useCORS === true) { + _this39.img.crossOrigin = 'Anonymous'; + } + var self = _this39; + _this39.img.onload = function () { + self.loaded = true; + }; + _this39.img.onerror = function () { + svg.log('ERROR: image "' + href + '" not found'); + self.loaded = true; + }; + _this39.img.src = href; + } else { + svg.ajax(href, true).then(function (img) { + _this39.img = img; + _this39.loaded = true; + }); + } + return _this39; + } + + createClass(_class35, [{ + key: 'renderChildren', + value: function renderChildren(ctx) { + var x = this.attribute('x').toPixels('x'); + var y = this.attribute('y').toPixels('y'); + + var width = this.attribute('width').toPixels('x'); + var height = this.attribute('height').toPixels('y'); + if (width === 0 || height === 0) return; + + ctx.save(); + if (this._isSvg) { + ctx.drawSvg(this.img, x, y, width, height); + } else { + ctx.translate(x, y); + svg.AspectRatio(ctx, this.attribute('preserveAspectRatio').value, width, this.img.width, height, this.img.height, 0, 0); + ctx.drawImage(this.img, 0, 0); + } + ctx.restore(); + } + }, { + key: 'getBoundingBox', + value: function getBoundingBox() { + var x = this.attribute('x').toPixels('x'); + var y = this.attribute('y').toPixels('y'); + var width = this.attribute('width').toPixels('x'); + var height = this.attribute('height').toPixels('y'); + return new svg.BoundingBox(x, y, x + width, y + height); + } + }]); + return _class35; + }(svg.Element.RenderedElementBase); + + // group element + svg.Element.g = function (_svg$Element$Rendered6) { + inherits(_class36, _svg$Element$Rendered6); + + function _class36() { + classCallCheck(this, _class36); + return possibleConstructorReturn(this, (_class36.__proto__ || Object.getPrototypeOf(_class36)).apply(this, arguments)); + } + + createClass(_class36, [{ + key: 'getBoundingBox', + value: function getBoundingBox() { + var bb = new svg.BoundingBox(); + this.children.forEach(function (child) { + bb.addBoundingBox(child.getBoundingBox()); + }); + return bb; + } + }]); + return _class36; + }(svg.Element.RenderedElementBase); + + // symbol element + svg.Element.symbol = function (_svg$Element$Rendered7) { + inherits(_class37, _svg$Element$Rendered7); + + function _class37() { + classCallCheck(this, _class37); + return possibleConstructorReturn(this, (_class37.__proto__ || Object.getPrototypeOf(_class37)).apply(this, arguments)); + } + + createClass(_class37, [{ + key: 'render', + value: function render(ctx) { + // NO RENDER + } + }]); + return _class37; + }(svg.Element.RenderedElementBase); + + // style element + svg.Element.style = function (_svg$Element$ElementB10) { + inherits(_class38, _svg$Element$ElementB10); + + function _class38(node) { + classCallCheck(this, _class38); + + // text, or spaces then CDATA + var _this42 = possibleConstructorReturn(this, (_class38.__proto__ || Object.getPrototypeOf(_class38)).call(this, node)); + + var css = ''; + [].concat(toConsumableArray(node.childNodes)).forEach(function (_ref3) { + var nodeValue = _ref3.nodeValue; + + css += nodeValue; + }); + css = css.replace(/(\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, ''); // remove comments + css = svg.compressSpaces(css); // replace whitespace + var cssDefs = css.split('}'); + cssDefs.forEach(function (cssDef) { + if (svg.trim(cssDef) !== '') { + var _cssDef$split = cssDef.split('{'), + _cssDef$split2 = slicedToArray(_cssDef$split, 2), + cssClasses = _cssDef$split2[0], + cssProps = _cssDef$split2[1]; + + cssClasses = cssClasses.split(','); + cssProps = cssProps.split(';'); + cssClasses.forEach(function (cssClass) { + cssClass = svg.trim(cssClass); + if (cssClass !== '') { + var props = {}; + cssProps.forEach(function (cssProp) { + var prop = cssProp.indexOf(':'); + var name = cssProp.substr(0, prop); + var value = cssProp.substr(prop + 1, cssProp.length - prop); + if (name != null && value != null) { + props[svg.trim(name)] = new svg.Property(svg.trim(name), svg.trim(value)); + } + }); + svg.Styles[cssClass] = props; + if (cssClass === '@font-face') { + var fontFamily = props['font-family'].value.replace(/"/g, ''); + var srcs = props['src'].value.split(','); + srcs.forEach(function (src) { + if (src.includes('format("svg")')) { + var urlStart = src.indexOf('url'); + var urlEnd = src.indexOf(')', urlStart); + var url = src.substr(urlStart + 5, urlEnd - urlStart - 6); + // Can this ajax safely be converted to async? + var doc = svg.parseXml(svg.ajax(url)); + var fonts = doc.getElementsByTagName('font'); + [].concat(toConsumableArray(fonts)).forEach(function (font) { + font = svg.CreateElement(font); + svg.Definitions[fontFamily] = font; + }); + } + }); + } + } + }); + } + }); + return _this42; + } + + return _class38; + }(svg.Element.ElementBase); + + // use element + svg.Element.use = function (_svg$Element$Rendered8) { + inherits(_class39, _svg$Element$Rendered8); + + function _class39(node) { + classCallCheck(this, _class39); + + var _this43 = possibleConstructorReturn(this, (_class39.__proto__ || Object.getPrototypeOf(_class39)).call(this, node)); + + _this43._el = _this43.getHrefAttribute().getDefinition(); + return _this43; + } + + createClass(_class39, [{ + key: 'setContext', + value: function setContext(ctx) { + get(_class39.prototype.__proto__ || Object.getPrototypeOf(_class39.prototype), 'setContext', this).call(this, ctx); + if (this.attribute('x').hasValue()) ctx.translate(this.attribute('x').toPixels('x'), 0); + if (this.attribute('y').hasValue()) ctx.translate(0, this.attribute('y').toPixels('y')); + } + }, { + key: 'path', + value: function path(ctx) { + var element = this._el; + + if (element != null) element.path(ctx); + } + }, { + key: 'getBoundingBox', + value: function getBoundingBox() { + var element = this._el; + + if (element != null) return element.getBoundingBox(); + } + }, { + key: 'renderChildren', + value: function renderChildren(ctx) { + var element = this._el; + + if (element != null) { + var tempSvg = element; + if (element.type === 'symbol') { + // render me using a temporary svg element in symbol cases (https://www.w3.org/TR/SVG/struct.html#UseElement) + tempSvg = new svg.Element.svg(); + tempSvg.type = 'svg'; + tempSvg.attributes['viewBox'] = new svg.Property('viewBox', element.attribute('viewBox').value); + tempSvg.attributes['preserveAspectRatio'] = new svg.Property('preserveAspectRatio', element.attribute('preserveAspectRatio').value); + tempSvg.attributes['overflow'] = new svg.Property('overflow', element.attribute('overflow').value); + tempSvg.children = element.children; + } + if (tempSvg.type === 'svg') { + // if symbol or svg, inherit width/height from me + if (this.attribute('width').hasValue()) tempSvg.attributes['width'] = new svg.Property('width', this.attribute('width').value); + if (this.attribute('height').hasValue()) tempSvg.attributes['height'] = new svg.Property('height', this.attribute('height').value); + } + var oldParent = tempSvg.parent; + tempSvg.parent = null; + tempSvg.render(ctx); + tempSvg.parent = oldParent; + } + } + }]); + return _class39; + }(svg.Element.RenderedElementBase); + + // mask element + svg.Element.mask = function (_svg$Element$ElementB11) { + inherits(_class40, _svg$Element$ElementB11); + + function _class40() { + classCallCheck(this, _class40); + return possibleConstructorReturn(this, (_class40.__proto__ || Object.getPrototypeOf(_class40)).apply(this, arguments)); + } + + createClass(_class40, [{ + key: 'apply', + value: function apply(ctx, element) { + // render as temp svg + var x = this.attribute('x').toPixels('x'); + var y = this.attribute('y').toPixels('y'); + var width = this.attribute('width').toPixels('x'); + var height = this.attribute('height').toPixels('y'); + + if (width === 0 && height === 0) { + var bb = new svg.BoundingBox(); + this.children.forEach(function (child) { + bb.addBoundingBox(child.getBoundingBox()); + }); + x = Math.floor(bb.x1); + y = Math.floor(bb.y1); + width = Math.floor(bb.width()); + height = Math.floor(bb.height()); + } + + // temporarily remove mask to avoid recursion + var mask = element.attribute('mask').value; + element.attribute('mask').value = ''; + + var cMask = document.createElement('canvas'); + cMask.width = x + width; + cMask.height = y + height; + var maskCtx = cMask.getContext('2d'); + this.renderChildren(maskCtx); + + var c = document.createElement('canvas'); + c.width = x + width; + c.height = y + height; + var tempCtx = c.getContext('2d'); + element.render(tempCtx); + tempCtx.globalCompositeOperation = 'destination-in'; + tempCtx.fillStyle = maskCtx.createPattern(cMask, 'no-repeat'); + tempCtx.fillRect(0, 0, x + width, y + height); + + ctx.fillStyle = tempCtx.createPattern(c, 'no-repeat'); + ctx.fillRect(0, 0, x + width, y + height); + + // reassign mask + element.attribute('mask').value = mask; + } + }, { + key: 'render', + value: function render(ctx) { + // NO RENDER + } + }]); + return _class40; + }(svg.Element.ElementBase); + + // clip element + svg.Element.clipPath = function (_svg$Element$ElementB12) { + inherits(_class41, _svg$Element$ElementB12); + + function _class41() { + classCallCheck(this, _class41); + return possibleConstructorReturn(this, (_class41.__proto__ || Object.getPrototypeOf(_class41)).apply(this, arguments)); + } + + createClass(_class41, [{ + key: 'apply', + value: function apply(ctx) { + this.children.forEach(function (child) { + if (typeof child.path !== 'undefined') { + var transform = null; + if (child.attribute('transform').hasValue()) { + transform = new svg.Transform(child.attribute('transform').value); + transform.apply(ctx); + } + child.path(ctx); + ctx.clip(); + if (transform) { + transform.unapply(ctx); + } + } + }); + } + }, { + key: 'render', + value: function render(ctx) { + // NO RENDER + } + }]); + return _class41; + }(svg.Element.ElementBase); + + // filters + svg.Element.filter = function (_svg$Element$ElementB13) { + inherits(_class42, _svg$Element$ElementB13); + + function _class42() { + classCallCheck(this, _class42); + return possibleConstructorReturn(this, (_class42.__proto__ || Object.getPrototypeOf(_class42)).apply(this, arguments)); + } + + createClass(_class42, [{ + key: 'apply', + value: function apply(ctx, element) { + // render as temp svg + var bb = element.getBoundingBox(); + var x = Math.floor(bb.x1); + var y = Math.floor(bb.y1); + var width = Math.floor(bb.width()); + var height = Math.floor(bb.height()); + + // temporarily remove filter to avoid recursion + var filter = element.style('filter').value; + element.style('filter').value = ''; + + var px = 0, + py = 0; + this.children.forEach(function (child) { + var efd = child.extraFilterDistance || 0; + px = Math.max(px, efd); + py = Math.max(py, efd); + }); + + var c = document.createElement('canvas'); + c.width = width + 2 * px; + c.height = height + 2 * py; + var tempCtx = c.getContext('2d'); + tempCtx.translate(-x + px, -y + py); + element.render(tempCtx); + + // apply filters + this.children.forEach(function (child) { + child.apply(tempCtx, 0, 0, width + 2 * px, height + 2 * py); + }); + + // render on me + ctx.drawImage(c, 0, 0, width + 2 * px, height + 2 * py, x - px, y - py, width + 2 * px, height + 2 * py); + + // reassign filter + element.style('filter', true).value = filter; + } + }, { + key: 'render', + value: function render(ctx) { + // NO RENDER + } + }]); + return _class42; + }(svg.Element.ElementBase); + + svg.Element.feMorphology = function (_svg$Element$ElementB14) { + inherits(_class43, _svg$Element$ElementB14); + + function _class43() { + classCallCheck(this, _class43); + return possibleConstructorReturn(this, (_class43.__proto__ || Object.getPrototypeOf(_class43)).apply(this, arguments)); + } + + createClass(_class43, [{ + key: 'apply', + value: function apply(ctx, x, y, width, height) { + // TODO: implement + } + }]); + return _class43; + }(svg.Element.ElementBase); + + svg.Element.feComposite = function (_svg$Element$ElementB15) { + inherits(_class44, _svg$Element$ElementB15); + + function _class44() { + classCallCheck(this, _class44); + return possibleConstructorReturn(this, (_class44.__proto__ || Object.getPrototypeOf(_class44)).apply(this, arguments)); + } + + createClass(_class44, [{ + key: 'apply', + value: function apply(ctx, x, y, width, height) { + // TODO: implement + } + }]); + return _class44; + }(svg.Element.ElementBase); + + function imGet(img, x, y, width, height, rgba) { + return img[y * width * 4 + x * 4 + rgba]; + } + + function imSet(img, x, y, width, height, rgba, val) { + img[y * width * 4 + x * 4 + rgba] = val; + } + + svg.Element.feColorMatrix = function (_svg$Element$ElementB16) { + inherits(_class45, _svg$Element$ElementB16); + + function _class45(node) { + classCallCheck(this, _class45); + + var _this49 = possibleConstructorReturn(this, (_class45.__proto__ || Object.getPrototypeOf(_class45)).call(this, node)); + + var matrix = svg.ToNumberArray(_this49.attribute('values').value); + switch (_this49.attribute('type').valueOrDefault('matrix')) {// https://www.w3.org/TR/SVG/filters.html#feColorMatrixElement + case 'saturate': + var s = matrix[0]; + matrix = [0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0, 0, 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0, 0, 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1]; + break; + case 'hueRotate': + var a = matrix[0] * Math.PI / 180.0; + var _c3 = function _c3(m1, m2, m3) { + return m1 + Math.cos(a) * m2 + Math.sin(a) * m3; + }; + matrix = [_c3(0.213, 0.787, -0.213), _c3(0.715, -0.715, -0.715), _c3(0.072, -0.072, 0.928), 0, 0, _c3(0.213, -0.213, 0.143), _c3(0.715, 0.285, 0.140), _c3(0.072, -0.072, -0.283), 0, 0, _c3(0.213, -0.213, -0.787), _c3(0.715, -0.715, 0.715), _c3(0.072, 0.928, 0.072), 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1]; + break; + case 'luminanceToAlpha': + matrix = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2125, 0.7154, 0.0721, 0, 0, 0, 0, 0, 0, 1]; + break; + } + _this49.matrix = matrix; + + _this49._m = function (i, v) { + var mi = matrix[i]; + return mi * (mi < 0 ? v - 255 : v); + }; + return _this49; + } + + createClass(_class45, [{ + key: 'apply', + value: function apply(ctx, x, y, width, height) { + var m = this._m; + // assuming x==0 && y==0 for now + + var srcData = ctx.getImageData(0, 0, width, height); + for (var _y2 = 0; _y2 < height; _y2++) { + for (var _x2 = 0; _x2 < width; _x2++) { + var _r3 = imGet(srcData.data, _x2, _y2, width, height, 0); + var g = imGet(srcData.data, _x2, _y2, width, height, 1); + var _b2 = imGet(srcData.data, _x2, _y2, width, height, 2); + var a = imGet(srcData.data, _x2, _y2, width, height, 3); + imSet(srcData.data, _x2, _y2, width, height, 0, m(0, _r3) + m(1, g) + m(2, _b2) + m(3, a) + m(4, 1)); + imSet(srcData.data, _x2, _y2, width, height, 1, m(5, _r3) + m(6, g) + m(7, _b2) + m(8, a) + m(9, 1)); + imSet(srcData.data, _x2, _y2, width, height, 2, m(10, _r3) + m(11, g) + m(12, _b2) + m(13, a) + m(14, 1)); + imSet(srcData.data, _x2, _y2, width, height, 3, m(15, _r3) + m(16, g) + m(17, _b2) + m(18, a) + m(19, 1)); + } + } + ctx.clearRect(0, 0, width, height); + ctx.putImageData(srcData, 0, 0); + } + }]); + return _class45; + }(svg.Element.ElementBase); + + svg.Element.feGaussianBlur = function (_svg$Element$ElementB17) { + inherits(_class46, _svg$Element$ElementB17); + + function _class46(node) { + classCallCheck(this, _class46); + + var _this50 = possibleConstructorReturn(this, (_class46.__proto__ || Object.getPrototypeOf(_class46)).call(this, node)); + + _this50.blurRadius = Math.floor(_this50.attribute('stdDeviation').numValue()); + _this50.extraFilterDistance = _this50.blurRadius; + return _this50; + } + + createClass(_class46, [{ + key: 'apply', + value: function apply(ctx, x, y, width, height) { + if (typeof canvasRGBA_ === 'undefined') { + svg.log('ERROR: `setStackBlurCanvasRGBA` must be run for blur to work'); + return; + } + + // Todo: This might not be a problem anymore with out `instanceof` fix + // StackBlur requires canvas be on document + ctx.canvas.id = svg.UniqueId(); + ctx.canvas.style.display = 'none'; + document.body.append(ctx.canvas); + canvasRGBA_(ctx.canvas, x, y, width, height, this.blurRadius); + ctx.canvas.remove(); + } + }]); + return _class46; + }(svg.Element.ElementBase); + + // title element, do nothing + svg.Element.title = function (_svg$Element$ElementB18) { + inherits(_class47, _svg$Element$ElementB18); + + function _class47(node) { + classCallCheck(this, _class47); + return possibleConstructorReturn(this, (_class47.__proto__ || Object.getPrototypeOf(_class47)).call(this)); + } + + return _class47; + }(svg.Element.ElementBase); + + // desc element, do nothing + svg.Element.desc = function (_svg$Element$ElementB19) { + inherits(_class48, _svg$Element$ElementB19); + + function _class48(node) { + classCallCheck(this, _class48); + return possibleConstructorReturn(this, (_class48.__proto__ || Object.getPrototypeOf(_class48)).call(this)); + } + + return _class48; + }(svg.Element.ElementBase); + + svg.Element.MISSING = function (_svg$Element$ElementB20) { + inherits(_class49, _svg$Element$ElementB20); + + function _class49(node) { + classCallCheck(this, _class49); + + var _this53 = possibleConstructorReturn(this, (_class49.__proto__ || Object.getPrototypeOf(_class49)).call(this)); + + svg.log('ERROR: Element \'' + node.nodeName + '\' not yet implemented.'); + return _this53; + } + + return _class49; + }(svg.Element.ElementBase); + + // element factory + svg.CreateElement = function (node) { + var className = node.nodeName.replace(/^[^:]+:/, '') // remove namespace + .replace(/-/g, ''); // remove dashes + var e = void 0; + if (typeof svg.Element[className] !== 'undefined') { + e = new svg.Element[className](node); + } else { + e = new svg.Element.MISSING(node); + } + + e.type = node.nodeName; + return e; + }; + + // load from url + svg.load = function () { + var _ref4 = asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(ctx, url) { + var dom; + return regeneratorRuntime.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + _context.next = 2; + return svg.ajax(url, true); + + case 2: + dom = _context.sent; + return _context.abrupt('return', svg.loadXml(ctx, dom)); + + case 4: + case 'end': + return _context.stop(); + } + } + }, _callee, this); + })); + + return function (_x3, _x4) { + return _ref4.apply(this, arguments); + }; + }(); + + // load from xml + svg.loadXml = function (ctx, xml) { + return svg.loadXmlDoc(ctx, svg.parseXml(xml)); + }; + + svg.loadXmlDoc = function (ctx, dom) { + svg.init(ctx); + + var mapXY = function mapXY(p) { + var e = ctx.canvas; + while (e) { + p.x -= e.offsetLeft; + p.y -= e.offsetTop; + e = e.offsetParent; + } + if (window.scrollX) p.x += window.scrollX; + if (window.scrollY) p.y += window.scrollY; + return p; + }; + + // bind mouse + if (svg.opts.ignoreMouse !== true) { + ctx.canvas.onclick = function (e) { + var args = e != null ? [e.clientX, e.clientY] : [event.clientX, event.clientY]; + + var _mapXY = mapXY(new (Function.prototype.bind.apply(svg.Point, [null].concat(args)))()), + x = _mapXY.x, + y = _mapXY.y; + + svg.Mouse.onclick(x, y); + }; + ctx.canvas.onmousemove = function (e) { + var args = e != null ? [e.clientX, e.clientY] : [event.clientX, event.clientY]; + + var _mapXY2 = mapXY(new (Function.prototype.bind.apply(svg.Point, [null].concat(args)))()), + x = _mapXY2.x, + y = _mapXY2.y; + + svg.Mouse.onmousemove(x, y); + }; + } + + var e = svg.CreateElement(dom.documentElement); + e.root = true; + + // render loop + var isFirstRender = true; + var draw = function draw(resolve) { + svg.ViewPort.Clear(); + if (ctx.canvas.parentNode) { + svg.ViewPort.SetCurrent(ctx.canvas.parentNode.clientWidth, ctx.canvas.parentNode.clientHeight); + } + + if (svg.opts.ignoreDimensions !== true) { + // set canvas size + if (e.style('width').hasValue()) { + ctx.canvas.width = e.style('width').toPixels('x'); + ctx.canvas.style.width = ctx.canvas.width + 'px'; + } + if (e.style('height').hasValue()) { + ctx.canvas.height = e.style('height').toPixels('y'); + ctx.canvas.style.height = ctx.canvas.height + 'px'; + } + } + var cWidth = ctx.canvas.clientWidth || ctx.canvas.width; + var cHeight = ctx.canvas.clientHeight || ctx.canvas.height; + if (svg.opts.ignoreDimensions === true && e.style('width').hasValue() && e.style('height').hasValue()) { + cWidth = e.style('width').toPixels('x'); + cHeight = e.style('height').toPixels('y'); + } + svg.ViewPort.SetCurrent(cWidth, cHeight); + + if (svg.opts.offsetX != null) { + e.attribute('x', true).value = svg.opts.offsetX; + } + if (svg.opts.offsetY != null) { + e.attribute('y', true).value = svg.opts.offsetY; + } + if (svg.opts.scaleWidth != null || svg.opts.scaleHeight != null) { + var viewBox = svg.ToNumberArray(e.attribute('viewBox').value); + var xRatio = null, + yRatio = null; + + if (svg.opts.scaleWidth != null) { + if (e.attribute('width').hasValue()) { + xRatio = e.attribute('width').toPixels('x') / svg.opts.scaleWidth; + } else if (!isNaN(viewBox[2])) { + xRatio = viewBox[2] / svg.opts.scaleWidth; + } + } + + if (svg.opts.scaleHeight != null) { + if (e.attribute('height').hasValue()) { + yRatio = e.attribute('height').toPixels('y') / svg.opts.scaleHeight; + } else if (!isNaN(viewBox[3])) { + yRatio = viewBox[3] / svg.opts.scaleHeight; + } + } + + if (xRatio == null) { + xRatio = yRatio; + } + if (yRatio == null) { + yRatio = xRatio; + } + + e.attribute('width', true).value = svg.opts.scaleWidth; + e.attribute('height', true).value = svg.opts.scaleHeight; + e.attribute('viewBox', true).value = '0 0 ' + cWidth * xRatio + ' ' + cHeight * yRatio; + e.attribute('preserveAspectRatio', true).value = 'none'; + } + + // clear and render + if (svg.opts.ignoreClear !== true) { + ctx.clearRect(0, 0, cWidth, cHeight); + } + e.render(ctx); + if (isFirstRender) { + isFirstRender = false; + resolve(dom); + } + }; + + var waitingForImages = true; + svg.intervalID = setInterval(function () { + var needUpdate = false; + + if (waitingForImages && svg.ImagesLoaded()) { + waitingForImages = false; + needUpdate = true; + } + + // need update from mouse events? + if (svg.opts.ignoreMouse !== true) { + needUpdate = needUpdate | svg.Mouse.hasEvents(); + } + + // need update from animations? + if (svg.opts.ignoreAnimation !== true) { + svg.Animations.forEach(function (animation) { + needUpdate = needUpdate | animation.update(1000 / svg.FRAMERATE); + }); + } + + // need update from redraw? + if (typeof svg.opts.forceRedraw === 'function') { + if (svg.opts.forceRedraw() === true) { + needUpdate = true; + } + } + + // render if needed + if (needUpdate) { + draw(); + svg.Mouse.runEvents(); // run and clear our events + } + }, 1000 / svg.FRAMERATE); + return new Promise(function (resolve, reject) { + if (svg.ImagesLoaded()) { + waitingForImages = false; + draw(resolve); + } + }); + }; + + svg.stop = function () { + if (svg.intervalID) { + clearInterval(svg.intervalID); + } + }; + + svg.Mouse = { + events: [], + hasEvents: function hasEvents() { + return this.events.length !== 0; + }, + onclick: function onclick(x, y) { + this.events.push({ + type: 'onclick', x: x, y: y, + run: function run(e) { + if (e.onclick) e.onclick(); + } + }); + }, + onmousemove: function onmousemove(x, y) { + this.events.push({ + type: 'onmousemove', x: x, y: y, + run: function run(e) { + if (e.onmousemove) e.onmousemove(); + } + }); + }, + + + eventElements: [], + + checkPath: function checkPath(element, ctx) { + var _this54 = this; + + this.events.forEach(function (_ref5, i) { + var x = _ref5.x, + y = _ref5.y; + + if (ctx.isPointInPath && ctx.isPointInPath(x, y)) { + _this54.eventElements[i] = element; + } + }); + }, + checkBoundingBox: function checkBoundingBox(element, bb) { + var _this55 = this; + + this.events.forEach(function (_ref6, i) { + var x = _ref6.x, + y = _ref6.y; + + if (bb.isPointInBox(x, y)) { + _this55.eventElements[i] = element; + } + }); + }, + runEvents: function runEvents() { + var _this56 = this; + + svg.ctx.canvas.style.cursor = ''; + + this.events.forEach(function (e, i) { + var element = _this56.eventElements[i]; + while (element) { + e.run(element); + element = element.parent; + } + }); + + // done running, clear + this.events = []; + this.eventElements = []; + } + }; + + return svg; + } + + if (typeof CanvasRenderingContext2D !== 'undefined') { + CanvasRenderingContext2D.prototype.drawSvg = function (s, dx, dy, dw, dh) { + canvg(this.canvas, s, { + ignoreMouse: true, + ignoreAnimation: true, + ignoreDimensions: true, + ignoreClear: true, + offsetX: dx, + offsetY: dy, + scaleWidth: dw, + scaleHeight: dh + }); + }; + } + + exports.setStackBlurCanvasRGBA = setStackBlurCanvasRGBA; + exports.canvg = canvg; + + return exports; + +}({})); diff --git a/dist/dom-polyfill.js b/dist/dom-polyfill.js new file mode 100644 index 00000000..37c2375c --- /dev/null +++ b/dist/dom-polyfill.js @@ -0,0 +1,124 @@ +(function () { + 'use strict'; + + // From https://github.com/inexorabletash/polyfill/blob/master/dom.js + + function mixin(o, ps) { + if (!o) return; + Object.keys(ps).forEach(function (p) { + if (p in o || p in o.prototype) { + return; + } + try { + Object.defineProperty(o.prototype, p, Object.getOwnPropertyDescriptor(ps, p)); + } catch (ex) { + // Throws in IE8; just copy it + o[p] = ps[p]; + } + }); + } + + function convertNodesIntoANode(nodes) { + nodes = nodes.map(function (node) { + return !(node instanceof Node) ? document.createTextNode(node) : node; + }); + if (nodes.length === 1) { + return nodes[0]; + } + var node = document.createDocumentFragment(); + nodes.forEach(function (n) { + node.appendChild(n); + }); + return node; + } + + var ParentNode = { + prepend: function prepend() { + for (var _len = arguments.length, nodes = Array(_len), _key = 0; _key < _len; _key++) { + nodes[_key] = arguments[_key]; + } + + nodes = convertNodesIntoANode(nodes); + this.insertBefore(nodes, this.firstChild); + }, + append: function append() { + for (var _len2 = arguments.length, nodes = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + nodes[_key2] = arguments[_key2]; + } + + nodes = convertNodesIntoANode(nodes); + this.appendChild(nodes); + } + }; + + mixin(Document || HTMLDocument, ParentNode); // HTMLDocument for IE8 + mixin(DocumentFragment, ParentNode); + mixin(Element, ParentNode); + + // Mixin ChildNode + // https://dom.spec.whatwg.org/#interface-childnode + + var ChildNode = { + before: function before() { + var parent = this.parentNode; + if (!parent) return; + var viablePreviousSibling = this.previousSibling; + + for (var _len3 = arguments.length, nodes = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { + nodes[_key3] = arguments[_key3]; + } + + while (nodes.includes(viablePreviousSibling)) { + viablePreviousSibling = viablePreviousSibling.previousSibling; + } + var node = convertNodesIntoANode(nodes); + parent.insertBefore(node, viablePreviousSibling ? viablePreviousSibling.nextSibling : parent.firstChild); + }, + after: function after() { + var parent = this.parentNode; + if (!parent) return; + var viableNextSibling = this.nextSibling; + + for (var _len4 = arguments.length, nodes = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { + nodes[_key4] = arguments[_key4]; + } + + while (nodes.includes(viableNextSibling)) { + viableNextSibling = viableNextSibling.nextSibling; + } + var node = convertNodesIntoANode(nodes); + parent.insertBefore(node, viableNextSibling); + }, + replaceWith: function replaceWith() { + var parent = this.parentNode; + if (!parent) return; + var viableNextSibling = this.nextSibling; + + for (var _len5 = arguments.length, nodes = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { + nodes[_key5] = arguments[_key5]; + } + + while (nodes.includes(viableNextSibling)) { + viableNextSibling = viableNextSibling.nextSibling; + } + var node = convertNodesIntoANode(nodes); + + if (this.parentNode === parent) { + parent.replaceChild(node, this); + } else { + parent.insertBefore(node, viableNextSibling); + } + }, + remove: function remove() { + if (!this.parentNode) { + return; + } + this.parentNode.removeChild(this); + } + }; + + mixin(DocumentType, ChildNode); + mixin(Element, ChildNode); + mixin(CharacterData, ChildNode); + +}()); diff --git a/dist/extensions/ext-arrows.js b/dist/extensions/ext-arrows.js new file mode 100644 index 00000000..83e9615f --- /dev/null +++ b/dist/extensions/ext-arrows.js @@ -0,0 +1,369 @@ +var svgEditorExtension_arrows = (function () { + 'use strict'; + + var asyncToGenerator = function (fn) { + return function () { + var gen = fn.apply(this, arguments); + return new Promise(function (resolve, reject) { + function step(key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + return Promise.resolve(value).then(function (value) { + step("next", value); + }, function (err) { + step("throw", err); + }); + } + } + + return step("next"); + }); + }; + }; + + /* globals jQuery */ + /** + * ext-arrows.js + * + * @license MIT + * + * @copyright 2010 Alexis Deveria + * + */ + var extArrows = { + name: 'arrows', + init: function () { + var _ref = asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2(S) { + var strings, svgEditor, svgCanvas, $, addElem, nonce, prefix, selElems, arrowprefix, randomizeIds, setArrowNonce, unsetArrowNonce, pathdata, getLinked, showPanel, resetMarker, addMarker, setArrow, colorChanged, contextTools; + return regeneratorRuntime.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + colorChanged = function colorChanged(elem) { + var color = elem.getAttribute('stroke'); + var mtypes = ['start', 'mid', 'end']; + var defs = svgCanvas.findDefs(); + + $.each(mtypes, function (i, type) { + var marker = getLinked(elem, 'marker-' + type); + if (!marker) { + return; + } + + var curColor = $(marker).children().attr('fill'); + var curD = $(marker).children().attr('d'); + if (curColor === color) { + return; + } + + var allMarkers = $(defs).find('marker'); + var newMarker = null; + // Different color, check if already made + allMarkers.each(function () { + var attrs = $(this).children().attr(['fill', 'd']); + if (attrs.fill === color && attrs.d === curD) { + // Found another marker with this color and this path + newMarker = this; + } + }); + + if (!newMarker) { + // Create a new marker with this color + var lastId = marker.id; + var dir = lastId.includes('_fw') ? 'fw' : 'bk'; + + newMarker = addMarker(dir, type, arrowprefix + dir + allMarkers.length); + + $(newMarker).children().attr('fill', color); + } + + $(elem).attr('marker-' + type, 'url(#' + newMarker.id + ')'); + + // Check if last marker can be removed + var remove = true; + $(S.svgcontent).find('line, polyline, path, polygon').each(function () { + var elem = this; + $.each(mtypes, function (j, mtype) { + if ($(elem).attr('marker-' + mtype) === 'url(#' + marker.id + ')') { + remove = false; + return remove; + } + }); + if (!remove) { + return false; + } + }); + + // Not found, so can safely remove + if (remove) { + $(marker).remove(); + } + }); + }; + + setArrow = function setArrow() { + resetMarker(); + + var type = this.value; + if (type === 'none') { + return; + } + + // Set marker on element + var dir = 'fw'; + if (type === 'mid_bk') { + type = 'mid'; + dir = 'bk'; + } else if (type === 'both') { + addMarker('bk', type); + svgCanvas.changeSelectedAttribute('marker-start', 'url(#' + pathdata.bk.id + ')'); + type = 'end'; + dir = 'fw'; + } else if (type === 'start') { + dir = 'bk'; + } + + addMarker(dir, type); + svgCanvas.changeSelectedAttribute('marker-' + type, 'url(#' + pathdata[dir].id + ')'); + svgCanvas.call('changed', selElems); + }; + + addMarker = function addMarker(dir, type, id) { + // TODO: Make marker (or use?) per arrow type, since refX can be different + id = id || arrowprefix + dir; + + var data = pathdata[dir]; + + if (type === 'mid') { + data.refx = 5; + } + + var marker = svgCanvas.getElem(id); + if (!marker) { + marker = addElem({ + element: 'marker', + attr: { + viewBox: '0 0 10 10', + id: id, + refY: 5, + markerUnits: 'strokeWidth', + markerWidth: 5, + markerHeight: 5, + orient: 'auto', + style: 'pointer-events:none' // Currently needed for Opera + } + }); + var arrow = addElem({ + element: 'path', + attr: { + d: data.d, + fill: '#000000' + } + }); + marker.append(arrow); + svgCanvas.findDefs().append(marker); + } + + marker.setAttribute('refX', data.refx); + + return marker; + }; + + resetMarker = function resetMarker() { + var el = selElems[0]; + el.removeAttribute('marker-start'); + el.removeAttribute('marker-mid'); + el.removeAttribute('marker-end'); + }; + + showPanel = function showPanel(on) { + $('#arrow_panel').toggle(on); + if (on) { + var el = selElems[0]; + var end = el.getAttribute('marker-end'); + var start = el.getAttribute('marker-start'); + var mid = el.getAttribute('marker-mid'); + var val = void 0; + if (end && start) { + val = 'both'; + } else if (end) { + val = 'end'; + } else if (start) { + val = 'start'; + } else if (mid) { + val = 'mid'; + if (mid.includes('bk')) { + val = 'mid_bk'; + } + } + + if (!start && !mid && !end) { + val = 'none'; + } + + $('#arrow_list').val(val); + } + }; + + getLinked = function getLinked(elem, attr) { + var str = elem.getAttribute(attr); + if (!str) { + return null; + } + var m = str.match(/\(#(.*)\)/); + if (!m || m.length !== 2) { + return null; + } + return svgCanvas.getElem(m[1]); + }; + + unsetArrowNonce = function unsetArrowNonce(window) { + randomizeIds = false; + arrowprefix = prefix; + pathdata.fw.id = arrowprefix + 'fw'; + pathdata.bk.id = arrowprefix + 'bk'; + }; + + setArrowNonce = function setArrowNonce(window, n) { + randomizeIds = true; + arrowprefix = prefix + n + '_'; + pathdata.fw.id = arrowprefix + 'fw'; + pathdata.bk.id = arrowprefix + 'bk'; + }; + + _context2.next = 10; + return S.importLocale(); + + case 10: + strings = _context2.sent; + svgEditor = this; + svgCanvas = svgEditor.canvas; + $ = jQuery; + // {svgcontent} = S, + addElem = svgCanvas.addSVGElementFromJson, nonce = S.nonce, prefix = 'se_arrow_'; + selElems = void 0, arrowprefix = void 0, randomizeIds = S.randomize_ids; + + + svgCanvas.bind('setnonce', setArrowNonce); + svgCanvas.bind('unsetnonce', unsetArrowNonce); + + if (randomizeIds) { + arrowprefix = prefix + nonce + '_'; + } else { + arrowprefix = prefix; + } + + pathdata = { + fw: { d: 'm0,0l10,5l-10,5l5,-5l-5,-5z', refx: 8, id: arrowprefix + 'fw' }, + bk: { d: 'm10,0l-10,5l10,5l-5,-5l5,-5z', refx: 2, id: arrowprefix + 'bk' } + }; + contextTools = [{ + type: 'select', + panel: 'arrow_panel', + id: 'arrow_list', + defval: 'none', + events: { + change: setArrow + } + }]; + return _context2.abrupt('return', { + name: strings.name, + context_tools: strings.contextTools.map(function (contextTool, i) { + return Object.assign(contextTools[i], contextTool); + }), + callback: function callback() { + $('#arrow_panel').hide(); + // Set ID so it can be translated in locale file + $('#arrow_list option')[0].id = 'connector_no_arrow'; + }, + addLangData: function () { + var _ref3 = asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(_ref2) { + var lang = _ref2.lang, + importLocale = _ref2.importLocale; + var strings; + return regeneratorRuntime.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + _context.next = 2; + return importLocale(); + + case 2: + strings = _context.sent; + return _context.abrupt('return', { + data: strings.langList + }); + + case 4: + case 'end': + return _context.stop(); + } + } + }, _callee, this); + })); + + function addLangData(_x2) { + return _ref3.apply(this, arguments); + } + + return addLangData; + }(), + selectedChanged: function selectedChanged(opts) { + // Use this to update the current selected elements + selElems = opts.elems; + + var markerElems = ['line', 'path', 'polyline', 'polygon']; + var i = selElems.length; + while (i--) { + var elem = selElems[i]; + if (elem && markerElems.includes(elem.tagName)) { + if (opts.selectedElement && !opts.multiselected) { + showPanel(true); + } else { + showPanel(false); + } + } else { + showPanel(false); + } + } + }, + elementChanged: function elementChanged(opts) { + var elem = opts.elems[0]; + if (elem && (elem.getAttribute('marker-start') || elem.getAttribute('marker-mid') || elem.getAttribute('marker-end'))) { + // const start = elem.getAttribute('marker-start'); + // const mid = elem.getAttribute('marker-mid'); + // const end = elem.getAttribute('marker-end'); + // Has marker, so see if it should match color + colorChanged(elem); + } + } + }); + + case 22: + case 'end': + return _context2.stop(); + } + } + }, _callee2, this); + })); + + function init(_x) { + return _ref.apply(this, arguments); + } + + return init; + }() + }; + + return extArrows; + +}()); diff --git a/dist/extensions/ext-closepath.js b/dist/extensions/ext-closepath.js new file mode 100644 index 00000000..efa7cfd0 --- /dev/null +++ b/dist/extensions/ext-closepath.js @@ -0,0 +1,164 @@ +var svgEditorExtension_closepath = (function () { + 'use strict'; + + var asyncToGenerator = function (fn) { + return function () { + var gen = fn.apply(this, arguments); + return new Promise(function (resolve, reject) { + function step(key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + return Promise.resolve(value).then(function (value) { + step("next", value); + }, function (err) { + step("throw", err); + }); + } + } + + return step("next"); + }); + }; + }; + + /* globals jQuery */ + /** + * ext-closepath.js + * + * @license MIT + * + * @copyright 2010 Jeff Schiller + * + */ + + // This extension adds a simple button to the contextual panel for paths + // The button toggles whether the path is open or closed + var extClosepath = { + name: 'closepath', + init: function () { + var _ref2 = asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(_ref) { + var importLocale = _ref.importLocale; + var strings, $, svgEditor, selElems, updateButton, showPanel, toggleClosed, buttons; + return regeneratorRuntime.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + _context.next = 2; + return importLocale(); + + case 2: + strings = _context.sent; + $ = jQuery; + svgEditor = this; + selElems = void 0; + + updateButton = function updateButton(path) { + var seglist = path.pathSegList, + closed = seglist.getItem(seglist.numberOfItems - 1).pathSegType === 1, + showbutton = closed ? '#tool_openpath' : '#tool_closepath', + hidebutton = closed ? '#tool_closepath' : '#tool_openpath'; + $(hidebutton).hide(); + $(showbutton).show(); + }; + + showPanel = function showPanel(on) { + $('#closepath_panel').toggle(on); + if (on) { + var path = selElems[0]; + if (path) { + updateButton(path); + } + } + }; + + toggleClosed = function toggleClosed() { + var path = selElems[0]; + if (path) { + var seglist = path.pathSegList, + last = seglist.numberOfItems - 1; + // is closed + if (seglist.getItem(last).pathSegType === 1) { + seglist.removeItem(last); + } else { + seglist.appendItem(path.createSVGPathSegClosePath()); + } + updateButton(path); + } + }; + + buttons = [{ + id: 'tool_openpath', + icon: svgEditor.curConfig.extIconsPath + 'openpath.png', + type: 'context', + panel: 'closepath_panel', + events: { + click: function click() { + toggleClosed(); + } + } + }, { + id: 'tool_closepath', + icon: svgEditor.curConfig.extIconsPath + 'closepath.png', + type: 'context', + panel: 'closepath_panel', + events: { + click: function click() { + toggleClosed(); + } + } + }]; + return _context.abrupt('return', { + name: strings.name, + svgicons: svgEditor.curConfig.extIconsPath + 'closepath_icons.svg', + buttons: strings.buttons.map(function (button, i) { + return Object.assign(buttons[i], button); + }), + callback: function callback() { + $('#closepath_panel').hide(); + }, + selectedChanged: function selectedChanged(opts) { + selElems = opts.elems; + var i = selElems.length; + while (i--) { + var elem = selElems[i]; + if (elem && elem.tagName === 'path') { + if (opts.selectedElement && !opts.multiselected) { + showPanel(true); + } else { + showPanel(false); + } + } else { + showPanel(false); + } + } + } + }); + + case 11: + case 'end': + return _context.stop(); + } + } + }, _callee, this); + })); + + function init(_x) { + return _ref2.apply(this, arguments); + } + + return init; + }() + }; + + return extClosepath; + +}()); diff --git a/dist/extensions/ext-connector.js b/dist/extensions/ext-connector.js new file mode 100644 index 00000000..1d325cae --- /dev/null +++ b/dist/extensions/ext-connector.js @@ -0,0 +1,683 @@ +var svgEditorExtension_connector = (function () { + 'use strict'; + + var asyncToGenerator = function (fn) { + return function () { + var gen = fn.apply(this, arguments); + return new Promise(function (resolve, reject) { + function step(key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + return Promise.resolve(value).then(function (value) { + step("next", value); + }, function (err) { + step("throw", err); + }); + } + } + + return step("next"); + }); + }; + }; + + /* globals jQuery */ + /** + * ext-connector.js + * + * @license MIT + * + * @copyright 2010 Alexis Deveria + * + */ + + var extConnector = { + name: 'connector', + init: function () { + var _ref = asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2(S) { + var $, svgEditor, svgCanvas, getElem, svgroot, importLocale, addElem, selManager, connSel, elData, strings, startX, startY, curLine, startElem, endElem, seNs, svgcontent, started, connections, selElems, getBBintersect, getOffset, showPanel, setPoint, updateLine, findConnectors, updateConnectors, init, buttons; + return regeneratorRuntime.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + init = function init() { + // Make sure all connectors have data set + $(svgcontent).find('*').each(function () { + var conn = this.getAttributeNS(seNs, 'connector'); + if (conn) { + this.setAttribute('class', connSel.substr(1)); + var connData = conn.split(' '); + var sbb = svgCanvas.getStrokedBBox([getElem(connData[0])]); + var ebb = svgCanvas.getStrokedBBox([getElem(connData[1])]); + $(this).data('c_start', connData[0]).data('c_end', connData[1]).data('start_bb', sbb).data('end_bb', ebb); + svgCanvas.getEditorNS(true); + } + }); + // updateConnectors(); + }; + + updateConnectors = function updateConnectors(elems) { + // Updates connector lines based on selected elements + // Is not used on mousemove, as it runs getStrokedBBox every time, + // which isn't necessary there. + findConnectors(elems); + if (connections.length) { + // Update line with element + var i = connections.length; + while (i--) { + var conn = connections[i]; + var line = conn.connector; + var elem = conn.elem; + + // const sw = line.getAttribute('stroke-width') * 5; + + var pre = conn.is_start ? 'start' : 'end'; + + // Update bbox for this element + var bb = svgCanvas.getStrokedBBox([elem]); + bb.x = conn.start_x; + bb.y = conn.start_y; + elData(line, pre + '_bb', bb); + /* const addOffset = */elData(line, pre + '_off'); + + var altPre = conn.is_start ? 'end' : 'start'; + + // Get center pt of connected element + var bb2 = elData(line, altPre + '_bb'); + var srcX = bb2.x + bb2.width / 2; + var srcY = bb2.y + bb2.height / 2; + + // Set point of element being moved + var pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); + setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true); + + // Set point of connected element + var pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line)); + setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true); + + // Update points attribute manually for webkit + if (navigator.userAgent.includes('AppleWebKit')) { + var pts = line.points; + var len = pts.numberOfItems; + var ptArr = []; + for (var j = 0; j < len; j++) { + pt = pts.getItem(j); + ptArr[j] = pt.x + ',' + pt.y; + } + line.setAttribute('points', ptArr.join(' ')); + } + } + } + }; + + findConnectors = function findConnectors() { + var elems = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : selElems; + + var connectors = $(svgcontent).find(connSel); + connections = []; + + // Loop through connectors to see if one is connected to the element + connectors.each(function () { + var addThis = void 0; + function add() { + if (elems.includes(this)) { + // Pretend this element is selected + addThis = true; + } + } + + // Grab the ends + var parts = []; + ['start', 'end'].forEach(function (pos, i) { + var key = 'c_' + pos; + var part = elData(this, key); + if (part == null) { + part = document.getElementById(this.attributes['se:connector'].value.split(' ')[i]); + elData(this, 'c_' + pos, part.id); + elData(this, pos + '_bb', svgCanvas.getStrokedBBox([part])); + } else part = document.getElementById(part); + parts.push(part); + }.bind(this)); + + for (var i = 0; i < 2; i++) { + var cElem = parts[i]; + + addThis = false; + // The connected element might be part of a selected group + $(cElem).parents().each(add); + + if (!cElem || !cElem.parentNode) { + $(this).remove(); + continue; + } + if (elems.includes(cElem) || addThis) { + var bb = svgCanvas.getStrokedBBox([cElem]); + connections.push({ + elem: cElem, + connector: this, + is_start: i === 0, + start_x: bb.x, + start_y: bb.y + }); + } + } + }); + }; + + updateLine = function updateLine(diffX, diffY) { + // Update line with element + var i = connections.length; + while (i--) { + var conn = connections[i]; + var line = conn.connector; + // const {elem} = conn; + + var pre = conn.is_start ? 'start' : 'end'; + // const sw = line.getAttribute('stroke-width') * 5; + + // Update bbox for this element + var bb = elData(line, pre + '_bb'); + bb.x = conn.start_x + diffX; + bb.y = conn.start_y + diffY; + elData(line, pre + '_bb', bb); + + var altPre = conn.is_start ? 'end' : 'start'; + + // Get center pt of connected element + var bb2 = elData(line, altPre + '_bb'); + var srcX = bb2.x + bb2.width / 2; + var srcY = bb2.y + bb2.height / 2; + + // Set point of element being moved + var pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); // $(line).data(pre+'_off')?sw:0 + setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true); + + // Set point of connected element + var pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line)); + setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true); + } + }; + + setPoint = function setPoint(elem, pos, x, y, setMid) { + var pts = elem.points; + var pt = svgroot.createSVGPoint(); + pt.x = x; + pt.y = y; + if (pos === 'end') { + pos = pts.numberOfItems - 1; + } + // TODO: Test for this on init, then use alt only if needed + try { + pts.replaceItem(pt, pos); + } catch (err) { + // Should only occur in FF which formats points attr as "n,n n,n", so just split + var ptArr = elem.getAttribute('points').split(' '); + for (var i = 0; i < ptArr.length; i++) { + if (i === pos) { + ptArr[i] = x + ',' + y; + } + } + elem.setAttribute('points', ptArr.join(' ')); + } + + if (setMid) { + // Add center point + var ptStart = pts.getItem(0); + var ptEnd = pts.getItem(pts.numberOfItems - 1); + setPoint(elem, 1, (ptEnd.x + ptStart.x) / 2, (ptEnd.y + ptStart.y) / 2); + } + }; + + showPanel = function showPanel(on) { + var connRules = $('#connector_rules'); + if (!connRules.length) { + connRules = $('').appendTo('head'); + } + connRules.text(!on ? '' : '#tool_clone, #tool_topath, #tool_angle, #xy_panel { display: none !important; }'); + $('#connector_panel').toggle(on); + }; + + getOffset = function getOffset(side, line) { + var giveOffset = !!line.getAttribute('marker-' + side); + // const giveOffset = $(line).data(side+'_off'); + + // TODO: Make this number (5) be based on marker width/height + var size = line.getAttribute('stroke-width') * 5; + return giveOffset ? size : 0; + }; + + getBBintersect = function getBBintersect(x, y, bb, offset) { + if (offset) { + offset -= 0; + bb = $.extend({}, bb); + bb.width += offset; + bb.height += offset; + bb.x -= offset / 2; + bb.y -= offset / 2; + } + + var midX = bb.x + bb.width / 2; + var midY = bb.y + bb.height / 2; + var lenX = x - midX; + var lenY = y - midY; + + var slope = Math.abs(lenY / lenX); + + var ratio = void 0; + if (slope < bb.height / bb.width) { + ratio = bb.width / 2 / Math.abs(lenX); + } else { + ratio = lenY ? bb.height / 2 / Math.abs(lenY) : 0; + } + + return { + x: midX + lenX * ratio, + y: midY + lenY * ratio + }; + }; + + $ = jQuery; + svgEditor = this; + svgCanvas = svgEditor.canvas; + getElem = svgCanvas.getElem; + svgroot = S.svgroot, importLocale = S.importLocale, addElem = svgCanvas.addSVGElementFromJson, selManager = S.selectorManager, connSel = '.se_connector', elData = $.data; + _context2.next = 15; + return importLocale(); + + case 15: + strings = _context2.sent; + startX = void 0, startY = void 0, curLine = void 0, startElem = void 0, endElem = void 0, seNs = void 0, svgcontent = S.svgcontent, started = false, connections = [], selElems = []; + + /** + * + * @param {Element[]} [elem=selElems] Array of elements + */ + + // Do once + (function () { + var gse = svgCanvas.groupSelectedElements; + + svgCanvas.groupSelectedElements = function () { + svgCanvas.removeFromSelection($(connSel).toArray()); + return gse.apply(this, arguments); + }; + + var mse = svgCanvas.moveSelectedElements; + + svgCanvas.moveSelectedElements = function () { + var cmd = mse.apply(this, arguments); + updateConnectors(); + return cmd; + }; + + seNs = svgCanvas.getEditorNS(); + })(); + + // Do on reset + + + // $(svgroot).parent().mousemove(function (e) { + // // if (started + // // || svgCanvas.getMode() !== 'connector' + // // || e.target.parentNode.parentNode !== svgcontent) return; + // + // console.log('y') + // // if (e.target.parentNode.parentNode === svgcontent) { + // // + // // } + // }); + + buttons = [{ + id: 'mode_connect', + type: 'mode', + icon: svgEditor.curConfig.imgPath + 'cut.png', + includeWith: { + button: '#tool_line', + isDefault: false, + position: 1 + }, + events: { + click: function click() { + svgCanvas.setMode('connector'); + } + } + }]; + return _context2.abrupt('return', { + name: strings.name, + svgicons: svgEditor.curConfig.imgPath + 'conn.svg', + buttons: strings.buttons.map(function (button, i) { + return Object.assign(buttons[i], button); + }), + addLangData: function () { + var _ref3 = asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(_ref2) { + var lang = _ref2.lang, + importLocale = _ref2.importLocale; + return regeneratorRuntime.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + return _context.abrupt('return', { + data: strings.langList + }); + + case 1: + case 'end': + return _context.stop(); + } + } + }, _callee, this); + })); + + function addLangData(_x3) { + return _ref3.apply(this, arguments); + } + + return addLangData; + }(), + mouseDown: function mouseDown(opts) { + var e = opts.event; + startX = opts.start_x; + startY = opts.start_y; + var mode = svgCanvas.getMode(); + var initStroke = svgEditor.curConfig.initStroke; + + + if (mode === 'connector') { + if (started) { + return; + } + + var mouseTarget = e.target; + + var parents = $(mouseTarget).parents(); + + if ($.inArray(svgcontent, parents) !== -1) { + // Connectable element + + // If child of foreignObject, use parent + var fo = $(mouseTarget).closest('foreignObject'); + startElem = fo.length ? fo[0] : mouseTarget; + + // Get center of source element + var bb = svgCanvas.getStrokedBBox([startElem]); + var x = bb.x + bb.width / 2; + var y = bb.y + bb.height / 2; + + started = true; + curLine = addElem({ + element: 'polyline', + attr: { + id: svgCanvas.getNextId(), + points: x + ',' + y + ' ' + x + ',' + y + ' ' + startX + ',' + startY, + stroke: '#' + initStroke.color, + 'stroke-width': !startElem.stroke_width || startElem.stroke_width === 0 ? initStroke.width : startElem.stroke_width, + fill: 'none', + opacity: initStroke.opacity, + style: 'pointer-events:none' + } + }); + elData(curLine, 'start_bb', bb); + } + return { + started: true + }; + } + if (mode === 'select') { + findConnectors(); + } + }, + mouseMove: function mouseMove(opts) { + var zoom = svgCanvas.getZoom(); + // const e = opts.event; + var x = opts.mouse_x / zoom; + var y = opts.mouse_y / zoom; + + var diffX = x - startX, + diffY = y - startY; + + var mode = svgCanvas.getMode(); + + if (mode === 'connector' && started) { + // const sw = curLine.getAttribute('stroke-width') * 3; + // Set start point (adjusts based on bb) + var pt = getBBintersect(x, y, elData(curLine, 'start_bb'), getOffset('start', curLine)); + startX = pt.x; + startY = pt.y; + + setPoint(curLine, 0, pt.x, pt.y, true); + + // Set end point + setPoint(curLine, 'end', x, y, true); + } else if (mode === 'select') { + var slen = selElems.length; + while (slen--) { + var elem = selElems[slen]; + // Look for selected connector elements + if (elem && elData(elem, 'c_start')) { + // Remove the "translate" transform given to move + svgCanvas.removeFromSelection([elem]); + svgCanvas.getTransformList(elem).clear(); + } + } + if (connections.length) { + updateLine(diffX, diffY); + } + } + }, + mouseUp: function mouseUp(opts) { + // const zoom = svgCanvas.getZoom(); + var e = opts.event; + // , x = opts.mouse_x / zoom, + // , y = opts.mouse_y / zoom, + var mouseTarget = e.target; + + if (svgCanvas.getMode() !== 'connector') { + return; + } + var fo = $(mouseTarget).closest('foreignObject'); + if (fo.length) { + mouseTarget = fo[0]; + } + + var parents = $(mouseTarget).parents(); + + if (mouseTarget === startElem) { + // Start line through click + started = true; + return { + keep: true, + element: null, + started: started + }; + } + if ($.inArray(svgcontent, parents) === -1) { + // Not a valid target element, so remove line + $(curLine).remove(); + started = false; + return { + keep: false, + element: null, + started: started + }; + } + // Valid end element + endElem = mouseTarget; + + var startId = startElem.id, + endId = endElem.id; + var connStr = startId + ' ' + endId; + var altStr = endId + ' ' + startId; + // Don't create connector if one already exists + var dupe = $(svgcontent).find(connSel).filter(function () { + var conn = this.getAttributeNS(seNs, 'connector'); + if (conn === connStr || conn === altStr) { + return true; + } + }); + if (dupe.length) { + $(curLine).remove(); + return { + keep: false, + element: null, + started: false + }; + } + + var bb = svgCanvas.getStrokedBBox([endElem]); + + var pt = getBBintersect(startX, startY, bb, getOffset('start', curLine)); + setPoint(curLine, 'end', pt.x, pt.y, true); + $(curLine).data('c_start', startId).data('c_end', endId).data('end_bb', bb); + seNs = svgCanvas.getEditorNS(true); + curLine.setAttributeNS(seNs, 'se:connector', connStr); + curLine.setAttribute('class', connSel.substr(1)); + curLine.setAttribute('opacity', 1); + svgCanvas.addToSelection([curLine]); + svgCanvas.moveToBottomSelectedElement(); + selManager.requestSelector(curLine).showGrips(false); + started = false; + return { + keep: true, + element: curLine, + started: started + }; + }, + selectedChanged: function selectedChanged(opts) { + // TODO: Find better way to skip operations if no connectors are in use + if (!$(svgcontent).find(connSel).length) { + return; + } + + if (svgCanvas.getMode() === 'connector') { + svgCanvas.setMode('select'); + } + + // Use this to update the current selected elements + selElems = opts.elems; + + var i = selElems.length; + while (i--) { + var elem = selElems[i]; + if (elem && elData(elem, 'c_start')) { + selManager.requestSelector(elem).showGrips(false); + if (opts.selectedElement && !opts.multiselected) { + // TODO: Set up context tools and hide most regular line tools + showPanel(true); + } else { + showPanel(false); + } + } else { + showPanel(false); + } + } + updateConnectors(); + }, + elementChanged: function elementChanged(opts) { + var elem = opts.elems[0]; + if (elem && elem.tagName === 'svg' && elem.id === 'svgcontent') { + // Update svgcontent (can change on import) + svgcontent = elem; + init(); + } + + // Has marker, so change offset + if (elem && (elem.getAttribute('marker-start') || elem.getAttribute('marker-mid') || elem.getAttribute('marker-end'))) { + var start = elem.getAttribute('marker-start'); + var mid = elem.getAttribute('marker-mid'); + var end = elem.getAttribute('marker-end'); + curLine = elem; + $(elem).data('start_off', !!start).data('end_off', !!end); + + if (elem.tagName === 'line' && mid) { + // Convert to polyline to accept mid-arrow + + var x1 = Number(elem.getAttribute('x1')); + var x2 = Number(elem.getAttribute('x2')); + var y1 = Number(elem.getAttribute('y1')); + var y2 = Number(elem.getAttribute('y2')); + var _elem = elem, + id = _elem.id; + + + var midPt = ' ' + (x1 + x2) / 2 + ',' + (y1 + y2) / 2 + ' '; + var pline = addElem({ + element: 'polyline', + attr: { + points: x1 + ',' + y1 + midPt + x2 + ',' + y2, + stroke: elem.getAttribute('stroke'), + 'stroke-width': elem.getAttribute('stroke-width'), + 'marker-mid': mid, + fill: 'none', + opacity: elem.getAttribute('opacity') || 1 + } + }); + $(elem).after(pline).remove(); + svgCanvas.clearSelection(); + pline.id = id; + svgCanvas.addToSelection([pline]); + elem = pline; + } + } + // Update line if it's a connector + if (elem.getAttribute('class') === connSel.substr(1)) { + var _start = getElem(elData(elem, 'c_start')); + updateConnectors([_start]); + } else { + updateConnectors(); + } + }, + IDsUpdated: function IDsUpdated(input) { + var remove = []; + input.elems.forEach(function (elem) { + if ('se:connector' in elem.attr) { + elem.attr['se:connector'] = elem.attr['se:connector'].split(' ').map(function (oldID) { + return input.changes[oldID]; + }).join(' '); + + // Check validity - the field would be something like 'svg_21 svg_22', but + // if one end is missing, it would be 'svg_21' and therefore fail this test + if (!/. ./.test(elem.attr['se:connector'])) { + remove.push(elem.attr.id); + } + } + }); + return { remove: remove }; + }, + toolButtonStateUpdate: function toolButtonStateUpdate(opts) { + if (opts.nostroke) { + if ($('#mode_connect').hasClass('tool_button_current')) { + svgEditor.clickSelect(); + } + } + $('#mode_connect').toggleClass('disabled', opts.nostroke); + } + }); + + case 20: + case 'end': + return _context2.stop(); + } + } + }, _callee2, this); + })); + + function init(_x) { + return _ref.apply(this, arguments); + } + + return init; + }() + }; + + return extConnector; + +}()); diff --git a/dist/extensions/ext-eyedropper.js b/dist/extensions/ext-eyedropper.js new file mode 100644 index 00000000..0f9b50b9 --- /dev/null +++ b/dist/extensions/ext-eyedropper.js @@ -0,0 +1,185 @@ +var svgEditorExtension_eyedropper = (function () { + 'use strict'; + + var asyncToGenerator = function (fn) { + return function () { + var gen = fn.apply(this, arguments); + return new Promise(function (resolve, reject) { + function step(key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + return Promise.resolve(value).then(function (value) { + step("next", value); + }, function (err) { + step("throw", err); + }); + } + } + + return step("next"); + }); + }; + }; + + /* globals jQuery */ + /** + * ext-eyedropper.js + * + * @license MIT + * + * @copyright 2010 Jeff Schiller + * + */ + + var extEyedropper = { + name: 'eyedropper', + init: function () { + var _ref = asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(S) { + var strings, svgEditor, $, ChangeElementCommand, svgCanvas, addToHistory, currentStyle, getStyle, buttons; + return regeneratorRuntime.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + getStyle = function getStyle(opts) { + // if we are in eyedropper mode, we don't want to disable the eye-dropper tool + var mode = svgCanvas.getMode(); + if (mode === 'eyedropper') { + return; + } + + var tool = $('#tool_eyedropper'); + // enable-eye-dropper if one element is selected + var elem = null; + if (!opts.multiselected && opts.elems[0] && !['svg', 'g', 'use'].includes(opts.elems[0].nodeName)) { + elem = opts.elems[0]; + tool.removeClass('disabled'); + // grab the current style + currentStyle.fillPaint = elem.getAttribute('fill') || 'black'; + currentStyle.fillOpacity = elem.getAttribute('fill-opacity') || 1.0; + currentStyle.strokePaint = elem.getAttribute('stroke'); + currentStyle.strokeOpacity = elem.getAttribute('stroke-opacity') || 1.0; + currentStyle.strokeWidth = elem.getAttribute('stroke-width'); + currentStyle.strokeDashArray = elem.getAttribute('stroke-dasharray'); + currentStyle.strokeLinecap = elem.getAttribute('stroke-linecap'); + currentStyle.strokeLinejoin = elem.getAttribute('stroke-linejoin'); + currentStyle.opacity = elem.getAttribute('opacity') || 1.0; + // disable eye-dropper tool + } else { + tool.addClass('disabled'); + } + }; + + _context.next = 3; + return S.importLocale(); + + case 3: + strings = _context.sent; + svgEditor = this; + $ = jQuery; + ChangeElementCommand = S.ChangeElementCommand, svgCanvas = svgEditor.canvas, addToHistory = function addToHistory(cmd) { + svgCanvas.undoMgr.addCommandToHistory(cmd); + }, currentStyle = { + fillPaint: 'red', fillOpacity: 1.0, + strokePaint: 'black', strokeOpacity: 1.0, + strokeWidth: 5, strokeDashArray: null, + opacity: 1.0, + strokeLinecap: 'butt', + strokeLinejoin: 'miter' + }; + buttons = [{ + id: 'tool_eyedropper', + icon: svgEditor.curConfig.extIconsPath + 'eyedropper.png', + type: 'mode', + events: { + click: function click() { + svgCanvas.setMode('eyedropper'); + } + } + }]; + return _context.abrupt('return', { + name: strings.name, + svgicons: svgEditor.curConfig.extIconsPath + 'eyedropper-icon.xml', + buttons: strings.buttons.map(function (button, i) { + return Object.assign(buttons[i], button); + }), + + // if we have selected an element, grab its paint and enable the eye dropper button + selectedChanged: getStyle, + elementChanged: getStyle, + + mouseDown: function mouseDown(opts) { + var mode = svgCanvas.getMode(); + if (mode === 'eyedropper') { + var e = opts.event; + var target = e.target; + + if (!['svg', 'g', 'use'].includes(target.nodeName)) { + var changes = {}; + + var change = function change(elem, attrname, newvalue) { + changes[attrname] = elem.getAttribute(attrname); + elem.setAttribute(attrname, newvalue); + }; + + if (currentStyle.fillPaint) { + change(target, 'fill', currentStyle.fillPaint); + } + if (currentStyle.fillOpacity) { + change(target, 'fill-opacity', currentStyle.fillOpacity); + } + if (currentStyle.strokePaint) { + change(target, 'stroke', currentStyle.strokePaint); + } + if (currentStyle.strokeOpacity) { + change(target, 'stroke-opacity', currentStyle.strokeOpacity); + } + if (currentStyle.strokeWidth) { + change(target, 'stroke-width', currentStyle.strokeWidth); + } + if (currentStyle.strokeDashArray) { + change(target, 'stroke-dasharray', currentStyle.strokeDashArray); + } + if (currentStyle.opacity) { + change(target, 'opacity', currentStyle.opacity); + } + if (currentStyle.strokeLinecap) { + change(target, 'stroke-linecap', currentStyle.strokeLinecap); + } + if (currentStyle.strokeLinejoin) { + change(target, 'stroke-linejoin', currentStyle.strokeLinejoin); + } + + addToHistory(new ChangeElementCommand(target, changes)); + } + } + } + }); + + case 9: + case 'end': + return _context.stop(); + } + } + }, _callee, this); + })); + + function init(_x) { + return _ref.apply(this, arguments); + } + + return init; + }() + }; + + return extEyedropper; + +}()); diff --git a/dist/extensions/ext-foreignobject.js b/dist/extensions/ext-foreignobject.js new file mode 100644 index 00000000..13544a0f --- /dev/null +++ b/dist/extensions/ext-foreignobject.js @@ -0,0 +1,318 @@ +var svgEditorExtension_foreignobject = (function () { + 'use strict'; + + var asyncToGenerator = function (fn) { + return function () { + var gen = fn.apply(this, arguments); + return new Promise(function (resolve, reject) { + function step(key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + return Promise.resolve(value).then(function (value) { + step("next", value); + }, function (err) { + step("throw", err); + }); + } + } + + return step("next"); + }); + }; + }; + + /* globals jQuery */ + /** + * ext-foreignobject.js + * + * @license Apache-2.0 + * + * @copyright 2010 Jacques Distler, 2010 Alexis Deveria + * + */ + + var extForeignobject = { + name: 'foreignobject', + init: function () { + var _ref = asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(S) { + var svgEditor, text2xml, NS, importLocale, $, svgCanvas, svgdoc, strings, properlySourceSizeTextArea, showPanel, toggleSourceButtons, selElems, started, newFO, editingforeign, setForeignString, showForeignEditor, setAttr, buttons, contextTools; + return regeneratorRuntime.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + setAttr = function setAttr(attr, val) { + svgCanvas.changeSelectedAttribute(attr, val); + svgCanvas.call('changed', selElems); + }; + + showForeignEditor = function showForeignEditor() { + var elt = selElems[0]; + if (!elt || editingforeign) { + return; + } + editingforeign = true; + toggleSourceButtons(true); + elt.removeAttribute('fill'); + + var str = svgCanvas.svgToString(elt, 0); + $('#svg_source_textarea').val(str); + $('#svg_source_editor').fadeIn(); + properlySourceSizeTextArea(); + $('#svg_source_textarea').focus(); + }; + + setForeignString = function setForeignString(xmlString) { + var elt = selElems[0]; + try { + // convert string into XML document + var newDoc = text2xml('' + xmlString + ''); + // run it through our sanitizer to remove anything we do not support + svgCanvas.sanitizeSvg(newDoc.documentElement); + elt.replaceWith(svgdoc.importNode(newDoc.documentElement.firstChild, true)); + svgCanvas.call('changed', [elt]); + svgCanvas.clearSelection(); + } catch (e) { + console.log(e); + return false; + } + + return true; + }; + + toggleSourceButtons = function toggleSourceButtons(on) { + $('#tool_source_save, #tool_source_cancel').toggle(!on); + $('#foreign_save, #foreign_cancel').toggle(on); + }; + + showPanel = function showPanel(on) { + var fcRules = $('#fc_rules'); + if (!fcRules.length) { + fcRules = $('').appendTo('head'); + } + fcRules.text(!on ? '' : ' #tool_topath { display: none !important; }'); + $('#foreignObject_panel').toggle(on); + }; + + svgEditor = this; + text2xml = S.text2xml, NS = S.NS, importLocale = S.importLocale; + $ = jQuery; + svgCanvas = svgEditor.canvas; + svgdoc = S.svgroot.parentNode.ownerDocument; + _context.next = 12; + return importLocale(); + + case 12: + strings = _context.sent; + + properlySourceSizeTextArea = function properlySourceSizeTextArea() { + // TODO: remove magic numbers here and get values from CSS + var height = $('#svg_source_container').height() - 80; + $('#svg_source_textarea').css('height', height); + }; + + selElems = void 0, started = void 0, newFO = void 0, editingforeign = false; + + /** + * This function sets the content of element elt to the input XML. + * @param {string} xmlString - The XML text + * @param {Element} elt - the parent element to append to + * @returns {boolean} This function returns false if the set was unsuccessful, true otherwise. + */ + + buttons = [{ + id: 'tool_foreign', + icon: svgEditor.curConfig.extIconsPath + 'foreignobject-tool.png', + type: 'mode', + events: { + click: function click() { + svgCanvas.setMode('foreign'); + } + } + }, { + id: 'edit_foreign', + icon: svgEditor.curConfig.extIconsPath + 'foreignobject-edit.png', + type: 'context', + panel: 'foreignObject_panel', + events: { + click: function click() { + showForeignEditor(); + } + } + }]; + contextTools = [{ + type: 'input', + panel: 'foreignObject_panel', + id: 'foreign_width', + size: 3, + events: { + change: function change() { + setAttr('width', this.value); + } + } + }, { + type: 'input', + panel: 'foreignObject_panel', + id: 'foreign_height', + events: { + change: function change() { + setAttr('height', this.value); + } + } + }, { + type: 'input', + panel: 'foreignObject_panel', + id: 'foreign_font_size', + size: 2, + defval: 16, + events: { + change: function change() { + setAttr('font-size', this.value); + } + } + }]; + return _context.abrupt('return', { + name: strings.name, + svgicons: svgEditor.curConfig.extIconsPath + 'foreignobject-icons.xml', + buttons: strings.buttons.map(function (button, i) { + return Object.assign(buttons[i], button); + }), + context_tools: strings.contextTools.map(function (contextTool, i) { + return Object.assign(contextTools[i], contextTool); + }), + callback: function callback() { + $('#foreignObject_panel').hide(); + + var endChanges = function endChanges() { + $('#svg_source_editor').hide(); + editingforeign = false; + $('#svg_source_textarea').blur(); + toggleSourceButtons(false); + }; + + // TODO: Needs to be done after orig icon loads + setTimeout(function () { + // Create source save/cancel buttons + /* const save = */$('#tool_source_save').clone().hide().attr('id', 'foreign_save').unbind().appendTo('#tool_source_back').click(function () { + if (!editingforeign) { + return; + } + + if (!setForeignString($('#svg_source_textarea').val())) { + $.confirm('Errors found. Revert to original?', function (ok) { + if (!ok) { + return false; + } + endChanges(); + }); + } else { + endChanges(); + } + // setSelectMode(); + }); + + /* const cancel = */$('#tool_source_cancel').clone().hide().attr('id', 'foreign_cancel').unbind().appendTo('#tool_source_back').click(function () { + endChanges(); + }); + }, 3000); + }, + mouseDown: function mouseDown(opts) { + // const e = opts.event; + + if (svgCanvas.getMode() === 'foreign') { + started = true; + newFO = svgCanvas.addSVGElementFromJson({ + element: 'foreignObject', + attr: { + x: opts.start_x, + y: opts.start_y, + id: svgCanvas.getNextId(), + 'font-size': 16, // cur_text.font_size, + width: '48', + height: '20', + style: 'pointer-events:inherit' + } + }); + var m = svgdoc.createElementNS(NS.MATH, 'math'); + m.setAttributeNS(NS.XMLNS, 'xmlns', NS.MATH); + m.setAttribute('display', 'inline'); + var mi = svgdoc.createElementNS(NS.MATH, 'mi'); + mi.setAttribute('mathvariant', 'normal'); + mi.textContent = '\u03A6'; + var mo = svgdoc.createElementNS(NS.MATH, 'mo'); + mo.textContent = '\u222A'; + var mi2 = svgdoc.createElementNS(NS.MATH, 'mi'); + mi2.textContent = '\u2133'; + m.append(mi, mo, mi2); + newFO.append(m); + return { + started: true + }; + } + }, + mouseUp: function mouseUp(opts) { + // const e = opts.event; + if (svgCanvas.getMode() === 'foreign' && started) { + var attrs = $(newFO).attr(['width', 'height']); + var keep = attrs.width !== '0' || attrs.height !== '0'; + svgCanvas.addToSelection([newFO], true); + + return { + keep: keep, + element: newFO + }; + } + }, + selectedChanged: function selectedChanged(opts) { + // Use this to update the current selected elements + selElems = opts.elems; + + var i = selElems.length; + while (i--) { + var elem = selElems[i]; + if (elem && elem.tagName === 'foreignObject') { + if (opts.selectedElement && !opts.multiselected) { + $('#foreign_font_size').val(elem.getAttribute('font-size')); + $('#foreign_width').val(elem.getAttribute('width')); + $('#foreign_height').val(elem.getAttribute('height')); + showPanel(true); + } else { + showPanel(false); + } + } else { + showPanel(false); + } + } + }, + elementChanged: function elementChanged(opts) { + // const elem = opts.elems[0]; + } + }); + + case 18: + case 'end': + return _context.stop(); + } + } + }, _callee, this); + })); + + function init(_x) { + return _ref.apply(this, arguments); + } + + return init; + }() + }; + + return extForeignobject; + +}()); diff --git a/dist/extensions/ext-grid.js b/dist/extensions/ext-grid.js new file mode 100644 index 00000000..ee7ec5c2 --- /dev/null +++ b/dist/extensions/ext-grid.js @@ -0,0 +1,230 @@ +var svgEditorExtension_grid = (function () { + 'use strict'; + + var asyncToGenerator = function (fn) { + return function () { + var gen = fn.apply(this, arguments); + return new Promise(function (resolve, reject) { + function step(key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + return Promise.resolve(value).then(function (value) { + step("next", value); + }, function (err) { + step("throw", err); + }); + } + } + + return step("next"); + }); + }; + }; + + /* globals jQuery */ + /** + * ext-grid.js + * + * @license Apache-2.0 + * + * @copyright 2010 Redou Mine, 2010 Alexis Deveria + * + */ + + var extGrid = { + name: 'grid', + init: function () { + var _ref2 = asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(_ref) { + var NS = _ref.NS, + getTypeMap = _ref.getTypeMap, + importLocale = _ref.importLocale; + var strings, svgEditor, $, svgCanvas, svgdoc, assignAttributes, hcanvas, canvBG, units, intervals, showGrid, canvasGrid, gridPattern, gridimg, gridBox, updateGrid, gridUpdate, buttons; + return regeneratorRuntime.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + gridUpdate = function gridUpdate() { + if (showGrid) { + updateGrid(svgCanvas.getZoom()); + } + $('#canvasGrid').toggle(showGrid); + $('#view_grid').toggleClass('push_button_pressed tool_button'); + }; + + updateGrid = function updateGrid(zoom) { + // TODO: Try this with elements, then compare performance difference + var unit = units[svgEditor.curConfig.baseUnit]; // 1 = 1px + var uMulti = unit * zoom; + // Calculate the main number interval + var rawM = 100 / uMulti; + var multi = 1; + for (var i = 0; i < intervals.length; i++) { + var num = intervals[i]; + multi = num; + if (rawM <= num) { + break; + } + } + var bigInt = multi * uMulti; + + // Set the canvas size to the width of the container + hcanvas.width = bigInt; + hcanvas.height = bigInt; + var ctx = hcanvas.getContext('2d'); + var curD = 0.5; + var part = bigInt / 10; + + ctx.globalAlpha = 0.2; + ctx.strokeStyle = svgEditor.curConfig.gridColor; + for (var _i = 1; _i < 10; _i++) { + var subD = Math.round(part * _i) + 0.5; + // const lineNum = (i % 2)?12:10; + var lineNum = 0; + ctx.moveTo(subD, bigInt); + ctx.lineTo(subD, lineNum); + ctx.moveTo(bigInt, subD); + ctx.lineTo(lineNum, subD); + } + ctx.stroke(); + ctx.beginPath(); + ctx.globalAlpha = 0.5; + ctx.moveTo(curD, bigInt); + ctx.lineTo(curD, 0); + + ctx.moveTo(bigInt, curD); + ctx.lineTo(0, curD); + ctx.stroke(); + + var datauri = hcanvas.toDataURL('image/png'); + gridimg.setAttribute('width', bigInt); + gridimg.setAttribute('height', bigInt); + gridimg.parentNode.setAttribute('width', bigInt); + gridimg.parentNode.setAttribute('height', bigInt); + svgCanvas.setHref(gridimg, datauri); + }; + + _context.next = 4; + return importLocale(); + + case 4: + strings = _context.sent; + svgEditor = this; + $ = jQuery; + svgCanvas = svgEditor.canvas; + svgdoc = document.getElementById('svgcanvas').ownerDocument, assignAttributes = svgCanvas.assignAttributes, hcanvas = document.createElement('canvas'), canvBG = $('#canvasBackground'), units = getTypeMap(), intervals = [0.01, 0.1, 1, 10, 100, 1000]; + showGrid = svgEditor.curConfig.showGrid || false; + + + $(hcanvas).hide().appendTo('body'); + + canvasGrid = svgdoc.createElementNS(NS.SVG, 'svg'); + + assignAttributes(canvasGrid, { + id: 'canvasGrid', + width: '100%', + height: '100%', + x: 0, + y: 0, + overflow: 'visible', + display: 'none' + }); + canvBG.append(canvasGrid); + + // grid-pattern + gridPattern = svgdoc.createElementNS(NS.SVG, 'pattern'); + + assignAttributes(gridPattern, { + id: 'gridpattern', + patternUnits: 'userSpaceOnUse', + x: 0, // -(value.strokeWidth / 2), // position for strokewidth + y: 0, // -(value.strokeWidth / 2), // position for strokewidth + width: 100, + height: 100 + }); + + gridimg = svgdoc.createElementNS(NS.SVG, 'image'); + + assignAttributes(gridimg, { + x: 0, + y: 0, + width: 100, + height: 100 + }); + gridPattern.append(gridimg); + $('#svgroot defs').append(gridPattern); + + // grid-box + gridBox = svgdoc.createElementNS(NS.SVG, 'rect'); + + assignAttributes(gridBox, { + width: '100%', + height: '100%', + x: 0, + y: 0, + 'stroke-width': 0, + stroke: 'none', + fill: 'url(#gridpattern)', + style: 'pointer-events: none; display:visible;' + }); + $('#canvasGrid').append(gridBox); + + buttons = [{ + id: 'view_grid', + icon: svgEditor.curConfig.extIconsPath + 'grid.png', + type: 'context', + panel: 'editor_panel', + events: { + click: function click() { + svgEditor.curConfig.showGrid = showGrid = !showGrid; + gridUpdate(); + } + } + }]; + return _context.abrupt('return', { + name: strings.name, + svgicons: svgEditor.curConfig.extIconsPath + 'grid-icon.xml', + + zoomChanged: function zoomChanged(zoom) { + if (showGrid) { + updateGrid(zoom); + } + }, + callback: function callback() { + if (showGrid) { + gridUpdate(); + } + }, + + buttons: strings.buttons.map(function (button, i) { + return Object.assign(buttons[i], button); + }) + }); + + case 25: + case 'end': + return _context.stop(); + } + } + }, _callee, this); + })); + + function init(_x) { + return _ref2.apply(this, arguments); + } + + return init; + }() + }; + + return extGrid; + +}()); diff --git a/dist/extensions/ext-helloworld.js b/dist/extensions/ext-helloworld.js new file mode 100644 index 00000000..410b5b20 --- /dev/null +++ b/dist/extensions/ext-helloworld.js @@ -0,0 +1,193 @@ +var svgEditorExtension_helloworld = (function () { + 'use strict'; + + var asyncToGenerator = function (fn) { + return function () { + var gen = fn.apply(this, arguments); + return new Promise(function (resolve, reject) { + function step(key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + return Promise.resolve(value).then(function (value) { + step("next", value); + }, function (err) { + step("throw", err); + }); + } + } + + return step("next"); + }); + }; + }; + + var slicedToArray = function () { + function sliceIterator(arr, i) { + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"]) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; + } + + return function (arr, i) { + if (Array.isArray(arr)) { + return arr; + } else if (Symbol.iterator in Object(arr)) { + return sliceIterator(arr, i); + } else { + throw new TypeError("Invalid attempt to destructure non-iterable instance"); + } + }; + }(); + + /* globals jQuery */ + /** + * ext-helloworld.js + * + * @license MIT + * + * @copyright 2010 Alexis Deveria + * + */ + + /** + * This is a very basic SVG-Edit extension. It adds a "Hello World" button in + * the left ("mode") panel. Clicking on the button, and then the canvas + * will show the user the point on the canvas that was clicked on. + */ + var extHelloworld = { + name: 'helloworld', + init: function () { + var _ref2 = asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(_ref) { + var importLocale = _ref.importLocale; + var strings, svgEditor, $, svgCanvas; + return regeneratorRuntime.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + _context.next = 2; + return importLocale(); + + case 2: + strings = _context.sent; + svgEditor = this; + $ = jQuery; + svgCanvas = svgEditor.canvas; + return _context.abrupt('return', { + name: strings.name, + // For more notes on how to make an icon file, see the source of + // the helloworld-icon.xml + svgicons: svgEditor.curConfig.extIconsPath + 'helloworld-icon.xml', + + // Multiple buttons can be added in this array + buttons: [{ + // Must match the icon ID in helloworld-icon.xml + id: 'hello_world', + + // Fallback, e.g., for `file://` access + icon: svgEditor.curConfig.extIconsPath + 'helloworld.png', + + // This indicates that the button will be added to the "mode" + // button panel on the left side + type: 'mode', + + // Tooltip text + title: strings.buttons[0].title, + + // Events + events: { + click: function click() { + // The action taken when the button is clicked on. + // For "mode" buttons, any other button will + // automatically be de-pressed. + svgCanvas.setMode('hello_world'); + } + } + }], + // This is triggered when the main mouse button is pressed down + // on the editor canvas (not the tool panels) + mouseDown: function mouseDown() { + // Check the mode on mousedown + if (svgCanvas.getMode() === 'hello_world') { + // The returned object must include "started" with + // a value of true in order for mouseUp to be triggered + return { started: true }; + } + }, + + + // This is triggered from anywhere, but "started" must have been set + // to true (see above). Note that "opts" is an object with event info + mouseUp: function mouseUp(opts) { + // Check the mode on mouseup + if (svgCanvas.getMode() === 'hello_world') { + var zoom = svgCanvas.getZoom(); + + // Get the actual coordinate by dividing by the zoom value + var x = opts.mouse_x / zoom; + var y = opts.mouse_y / zoom; + + // We do our own formatting + var text = strings.text; + + [['x', x], ['y', y]].forEach(function (_ref3) { + var _ref4 = slicedToArray(_ref3, 2), + prop = _ref4[0], + val = _ref4[1]; + + text = text.replace('{' + prop + '}', val); + }); + + // Show the text using the custom alert function + $.alert(text); + } + } + }); + + case 7: + case 'end': + return _context.stop(); + } + } + }, _callee, this); + })); + + function init(_x) { + return _ref2.apply(this, arguments); + } + + return init; + }() + }; + + return extHelloworld; + +}()); diff --git a/dist/extensions/ext-imagelib.js b/dist/extensions/ext-imagelib.js new file mode 100644 index 00000000..1f81f9ff --- /dev/null +++ b/dist/extensions/ext-imagelib.js @@ -0,0 +1,429 @@ +var svgEditorExtension_imagelib = (function () { + 'use strict'; + + var asyncToGenerator = function (fn) { + return function () { + var gen = fn.apply(this, arguments); + return new Promise(function (resolve, reject) { + function step(key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + return Promise.resolve(value).then(function (value) { + step("next", value); + }, function (err) { + step("throw", err); + }); + } + } + + return step("next"); + }); + }; + }; + + /* globals jQuery */ + /** + * ext-imagelib.js + * + * @license MIT + * + * @copyright 2010 Alexis Deveria + * + */ + var extImagelib = { + name: 'imagelib', + init: function () { + var _ref2 = asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(_ref) { + var decode64 = _ref.decode64, + importLocale = _ref.importLocale; + var imagelibStrings, svgEditor, $, uiStrings, svgCanvas, closeBrowser, importImage, pending, mode, multiArr, transferStopped, preview, submit, toggleMulti, showBrowser, buttons; + return regeneratorRuntime.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + showBrowser = function showBrowser() { + var browser = $('#imgbrowse'); + if (!browser.length) { + $('
' + '
').insertAfter('#svg_docprops'); + browser = $('#imgbrowse'); + + var allLibs = imagelibStrings.select_lib; + + var libOpts = $('
    ').appendTo(browser); + var frame = $(' + + diff --git a/editor/browser.js b/editor/browser.js index dc2cafc4..0a5411f6 100644 --- a/editor/browser.js +++ b/editor/browser.js @@ -1,188 +1,275 @@ -/*globals $, svgedit*/ -/*jslint vars: true, eqeq: true*/ +/* globals jQuery */ /** - * Package: svgedit.browser + * Browser detection + * @module browser + * @license MIT * - * Licensed under the MIT License - * - * Copyright(c) 2010 Jeff Schiller - * Copyright(c) 2010 Alexis Deveria + * @copyright 2010 Jeff Schiller, 2010 Alexis Deveria */ // Dependencies: // 1) jQuery (for $.alert()) -(function() {'use strict'; +import './svgpathseg.js'; +import {NS} from './namespaces.js'; -if (!svgedit.browser) { - svgedit.browser = {}; -} +const $ = jQuery; -// alias -var NS = svgedit.NS; - -var supportsSvg_ = (function() { - return !!document.createElementNS && !!document.createElementNS(NS.SVG, 'svg').createSVGRect; +const supportsSVG_ = (function () { +return !!document.createElementNS && !!document.createElementNS(NS.SVG, 'svg').createSVGRect; }()); -svgedit.browser.supportsSvg = function() { return supportsSvg_; }; -if(!svgedit.browser.supportsSvg()) { - window.location = 'browser-not-supported.html'; - return; -} +/** + * @function module:browser.supportsSvg + * @returns {boolean} +*/ +export const supportsSvg = () => supportsSVG_; -var userAgent = navigator.userAgent; -var svg = document.createElementNS(NS.SVG, 'svg'); +const {userAgent} = navigator; +const svg = document.createElementNS(NS.SVG, 'svg'); // Note: Browser sniffing should only be used if no other detection method is possible -var isOpera_ = !!window.opera; -var isWebkit_ = userAgent.indexOf('AppleWebKit') >= 0; -var isGecko_ = userAgent.indexOf('Gecko/') >= 0; -var isIE_ = userAgent.indexOf('MSIE') >= 0; -var isChrome_ = userAgent.indexOf('Chrome/') >= 0; -var isWindows_ = userAgent.indexOf('Windows') >= 0; -var isMac_ = userAgent.indexOf('Macintosh') >= 0; -var isTouch_ = 'ontouchstart' in window; +const isOpera_ = !!window.opera; +const isWebkit_ = userAgent.includes('AppleWebKit'); +const isGecko_ = userAgent.includes('Gecko/'); +const isIE_ = userAgent.includes('MSIE'); +const isChrome_ = userAgent.includes('Chrome/'); +const isWindows_ = userAgent.includes('Windows'); +const isMac_ = userAgent.includes('Macintosh'); +const isTouch_ = 'ontouchstart' in window; -var supportsSelectors_ = (function() { - return !!svg.querySelector; +const supportsSelectors_ = (function () { +return !!svg.querySelector; }()); -var supportsXpath_ = (function() { - return !!document.evaluate; +const supportsXpath_ = (function () { +return !!document.evaluate; }()); // segList functions (for FF1.5 and 2.0) -var supportsPathReplaceItem_ = (function() { - var path = document.createElementNS(NS.SVG, 'path'); - path.setAttribute('d', 'M0,0 10,10'); - var seglist = path.pathSegList; - var seg = path.createSVGPathSegLinetoAbs(5,5); - try { - seglist.replaceItem(seg, 1); - return true; - } catch(err) {} - return false; +const supportsPathReplaceItem_ = (function () { +const path = document.createElementNS(NS.SVG, 'path'); +path.setAttribute('d', 'M0,0 10,10'); +const seglist = path.pathSegList; +const seg = path.createSVGPathSegLinetoAbs(5, 5); +try { + seglist.replaceItem(seg, 1); + return true; +} catch (err) {} +return false; }()); -var supportsPathInsertItemBefore_ = (function() { - var path = document.createElementNS(NS.SVG, 'path'); - path.setAttribute('d', 'M0,0 10,10'); - var seglist = path.pathSegList; - var seg = path.createSVGPathSegLinetoAbs(5,5); - try { - seglist.insertItemBefore(seg, 1); - return true; - } catch(err) {} - return false; +const supportsPathInsertItemBefore_ = (function () { +const path = document.createElementNS(NS.SVG, 'path'); +path.setAttribute('d', 'M0,0 10,10'); +const seglist = path.pathSegList; +const seg = path.createSVGPathSegLinetoAbs(5, 5); +try { + seglist.insertItemBefore(seg, 1); + return true; +} catch (err) {} +return false; }()); // text character positioning (for IE9) -var supportsGoodTextCharPos_ = (function() { - var svgroot = document.createElementNS(NS.SVG, 'svg'); - var svgcontent = document.createElementNS(NS.SVG, 'svg'); - document.documentElement.appendChild(svgroot); - svgcontent.setAttribute('x', 5); - svgroot.appendChild(svgcontent); - var text = document.createElementNS(NS.SVG, 'text'); - text.textContent = 'a'; - svgcontent.appendChild(text); - var pos = text.getStartPositionOfChar(0).x; - document.documentElement.removeChild(svgroot); - return (pos === 0); +const supportsGoodTextCharPos_ = (function () { +const svgroot = document.createElementNS(NS.SVG, 'svg'); +const svgcontent = document.createElementNS(NS.SVG, 'svg'); +document.documentElement.append(svgroot); +svgcontent.setAttribute('x', 5); +svgroot.append(svgcontent); +const text = document.createElementNS(NS.SVG, 'text'); +text.textContent = 'a'; +svgcontent.append(text); +const pos = text.getStartPositionOfChar(0).x; +svgroot.remove(); +return (pos === 0); }()); -var supportsPathBBox_ = (function() { - var svgcontent = document.createElementNS(NS.SVG, 'svg'); - document.documentElement.appendChild(svgcontent); - var path = document.createElementNS(NS.SVG, 'path'); - path.setAttribute('d', 'M0,0 C0,0 10,10 10,0'); - svgcontent.appendChild(path); - var bbox = path.getBBox(); - document.documentElement.removeChild(svgcontent); - return (bbox.height > 4 && bbox.height < 5); +const supportsPathBBox_ = (function () { +const svgcontent = document.createElementNS(NS.SVG, 'svg'); +document.documentElement.append(svgcontent); +const path = document.createElementNS(NS.SVG, 'path'); +path.setAttribute('d', 'M0,0 C0,0 10,10 10,0'); +svgcontent.append(path); +const bbox = path.getBBox(); +svgcontent.remove(); +return (bbox.height > 4 && bbox.height < 5); }()); // Support for correct bbox sizing on groups with horizontal/vertical lines -var supportsHVLineContainerBBox_ = (function() { - var svgcontent = document.createElementNS(NS.SVG, 'svg'); - document.documentElement.appendChild(svgcontent); - var path = document.createElementNS(NS.SVG, 'path'); - path.setAttribute('d', 'M0,0 10,0'); - var path2 = document.createElementNS(NS.SVG, 'path'); - path2.setAttribute('d', 'M5,0 15,0'); - var g = document.createElementNS(NS.SVG, 'g'); - g.appendChild(path); - g.appendChild(path2); - svgcontent.appendChild(g); - var bbox = g.getBBox(); - document.documentElement.removeChild(svgcontent); - // Webkit gives 0, FF gives 10, Opera (correctly) gives 15 - return (bbox.width == 15); +const supportsHVLineContainerBBox_ = (function () { +const svgcontent = document.createElementNS(NS.SVG, 'svg'); +document.documentElement.append(svgcontent); +const path = document.createElementNS(NS.SVG, 'path'); +path.setAttribute('d', 'M0,0 10,0'); +const path2 = document.createElementNS(NS.SVG, 'path'); +path2.setAttribute('d', 'M5,0 15,0'); +const g = document.createElementNS(NS.SVG, 'g'); +g.append(path, path2); +svgcontent.append(g); +const bbox = g.getBBox(); +svgcontent.remove(); +// Webkit gives 0, FF gives 10, Opera (correctly) gives 15 +return (bbox.width === 15); }()); -var supportsEditableText_ = (function() { - // TODO: Find better way to check support for this - return isOpera_; +const supportsEditableText_ = (function () { +// TODO: Find better way to check support for this +return isOpera_; }()); -var supportsGoodDecimals_ = (function() { - // Correct decimals on clone attributes (Opera < 10.5/win/non-en) - var rect = document.createElementNS(NS.SVG, 'rect'); - rect.setAttribute('x', 0.1); - var crect = rect.cloneNode(false); - var retValue = (crect.getAttribute('x').indexOf(',') == -1); - if(!retValue) { - $.alert('NOTE: This version of Opera is known to contain bugs in SVG-edit.\n'+ - 'Please upgrade to the latest version in which the problems have been fixed.'); - } - return retValue; +const supportsGoodDecimals_ = (function () { +// Correct decimals on clone attributes (Opera < 10.5/win/non-en) +const rect = document.createElementNS(NS.SVG, 'rect'); +rect.setAttribute('x', 0.1); +const crect = rect.cloneNode(false); +const retValue = (!crect.getAttribute('x').includes(',')); +if (!retValue) { + // Todo: i18nize or remove + $.alert('NOTE: This version of Opera is known to contain bugs in SVG-edit.\n' + + 'Please upgrade to the latest version in which the problems have been fixed.'); +} +return retValue; }()); -var supportsNonScalingStroke_ = (function() { - var rect = document.createElementNS(NS.SVG, 'rect'); - rect.setAttribute('style', 'vector-effect:non-scaling-stroke'); - return rect.style.vectorEffect === 'non-scaling-stroke'; +const supportsNonScalingStroke_ = (function () { +const rect = document.createElementNS(NS.SVG, 'rect'); +rect.setAttribute('style', 'vector-effect:non-scaling-stroke'); +return rect.style.vectorEffect === 'non-scaling-stroke'; }()); -var supportsNativeSVGTransformLists_ = (function() { - var rect = document.createElementNS(NS.SVG, 'rect'); - var rxform = rect.transform.baseVal; - var t1 = svg.createSVGTransform(); - rxform.appendItem(t1); - var r1 = rxform.getItem(0); - return r1 instanceof SVGTransform && t1 instanceof SVGTransform && - r1.type == t1.type && r1.angle == t1.angle && - r1.matrix.a == t1.matrix.a && - r1.matrix.b == t1.matrix.b && - r1.matrix.c == t1.matrix.c && - r1.matrix.d == t1.matrix.d && - r1.matrix.e == t1.matrix.e && - r1.matrix.f == t1.matrix.f; +let supportsNativeSVGTransformLists_ = (function () { +const rect = document.createElementNS(NS.SVG, 'rect'); +const rxform = rect.transform.baseVal; +const t1 = svg.createSVGTransform(); +rxform.appendItem(t1); +const r1 = rxform.getItem(0); +// Todo: Do frame-independent instance checking +return r1 instanceof SVGTransform && t1 instanceof SVGTransform && + r1.type === t1.type && r1.angle === t1.angle && + r1.matrix.a === t1.matrix.a && + r1.matrix.b === t1.matrix.b && + r1.matrix.c === t1.matrix.c && + r1.matrix.d === t1.matrix.d && + r1.matrix.e === t1.matrix.e && + r1.matrix.f === t1.matrix.f; }()); // Public API -svgedit.browser.isOpera = function() { return isOpera_; }; -svgedit.browser.isWebkit = function() { return isWebkit_; }; -svgedit.browser.isGecko = function() { return isGecko_; }; -svgedit.browser.isIE = function() { return isIE_; }; -svgedit.browser.isChrome = function() { return isChrome_; }; -svgedit.browser.isWindows = function() { return isWindows_; }; -svgedit.browser.isMac = function() { return isMac_; }; -svgedit.browser.isTouch = function() { return isTouch_; }; +/** + * @function module:browser.isOpera + * @returns {boolean} +*/ +export const isOpera = () => isOpera_; +/** + * @function module:browser.isWebkit + * @returns {boolean} +*/ +export const isWebkit = () => isWebkit_; +/** + * @function module:browser.isGecko + * @returns {boolean} +*/ +export const isGecko = () => isGecko_; +/** + * @function module:browser.isIE + * @returns {boolean} +*/ +export const isIE = () => isIE_; +/** + * @function module:browser.isChrome + * @returns {boolean} +*/ +export const isChrome = () => isChrome_; +/** + * @function module:browser.isWindows + * @returns {boolean} +*/ +export const isWindows = () => isWindows_; +/** + * @function module:browser.isMac + * @returns {boolean} +*/ +export const isMac = () => isMac_; +/** + * @function module:browser.isTouch + * @returns {boolean} +*/ +export const isTouch = () => isTouch_; -svgedit.browser.supportsSelectors = function() { return supportsSelectors_; }; -svgedit.browser.supportsXpath = function() { return supportsXpath_; }; +/** + * @function module:browser.supportsSelectors + * @returns {boolean} +*/ +export const supportsSelectors = () => supportsSelectors_; -svgedit.browser.supportsPathReplaceItem = function() { return supportsPathReplaceItem_; }; -svgedit.browser.supportsPathInsertItemBefore = function() { return supportsPathInsertItemBefore_; }; -svgedit.browser.supportsPathBBox = function() { return supportsPathBBox_; }; -svgedit.browser.supportsHVLineContainerBBox = function() { return supportsHVLineContainerBBox_; }; -svgedit.browser.supportsGoodTextCharPos = function() { return supportsGoodTextCharPos_; }; -svgedit.browser.supportsEditableText = function() { return supportsEditableText_; }; -svgedit.browser.supportsGoodDecimals = function() { return supportsGoodDecimals_; }; -svgedit.browser.supportsNonScalingStroke = function() { return supportsNonScalingStroke_; }; -svgedit.browser.supportsNativeTransformLists = function() { return supportsNativeSVGTransformLists_; }; +/** + * @function module:browser.supportsXpath + * @returns {boolean} +*/ +export const supportsXpath = () => supportsXpath_; -}()); +/** + * @function module:browser.supportsPathReplaceItem + * @returns {boolean} +*/ +export const supportsPathReplaceItem = () => supportsPathReplaceItem_; + +/** + * @function module:browser.supportsPathInsertItemBefore + * @returns {boolean} +*/ +export const supportsPathInsertItemBefore = () => supportsPathInsertItemBefore_; + +/** + * @function module:browser.supportsPathBBox + * @returns {boolean} +*/ +export const supportsPathBBox = () => supportsPathBBox_; + +/** + * @function module:browser.supportsHVLineContainerBBox + * @returns {boolean} +*/ +export const supportsHVLineContainerBBox = () => supportsHVLineContainerBBox_; + +/** + * @function module:browser.supportsGoodTextCharPos + * @returns {boolean} +*/ +export const supportsGoodTextCharPos = () => supportsGoodTextCharPos_; + +/** +* @function module:browser.supportsEditableText + * @returns {boolean} +*/ +export const supportsEditableText = () => supportsEditableText_; + +/** + * @function module:browser.supportsGoodDecimals + * @returns {boolean} +*/ +export const supportsGoodDecimals = () => supportsGoodDecimals_; + +/** +* @function module:browser.supportsNonScalingStroke +* @returns {boolean} +*/ +export const supportsNonScalingStroke = () => supportsNonScalingStroke_; + +/** +* @function module:browser.supportsNativeTransformLists +* @returns {boolean} +*/ +export const supportsNativeTransformLists = () => supportsNativeSVGTransformLists_; + +/** + * Set `supportsNativeSVGTransformLists_` to `false` (for unit testing) + * @function module:browser.disableSupportsNativeTransformLists + * @returns {undefined} +*/ +export const disableSupportsNativeTransformLists = () => { + supportsNativeSVGTransformLists_ = false; +}; diff --git a/editor/canvg/StackBlur.js b/editor/canvg/StackBlur.js new file mode 100644 index 00000000..4e3c59b2 --- /dev/null +++ b/editor/canvg/StackBlur.js @@ -0,0 +1,656 @@ +/** +* StackBlur - a fast almost Gaussian Blur For Canvas + +In case you find this class useful - especially in commercial projects - +I am not totally unhappy for a small donation to my PayPal account +mario@quasimondo.de + +Or support me on flattr: +https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript + +* @module StackBlur +* @version 0.5 +* @author Mario Klingemann +Contact: mario@quasimondo.com +Website: http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html +Twitter: @quasimondo + +* @copyright (c) 2010 Mario Klingemann + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +*/ + +const mulTable = [ + 512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512, + 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512, + 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456, + 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512, + 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328, + 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456, + 446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335, + 329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512, + 505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405, + 399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, + 324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, + 268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, + 451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, + 385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, + 332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, + 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259]; + +const shgTable = [ + 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, + 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24]; + +/** + * @param {string|HTMLImageElement} img + * @param {string|HTMLCanvasElement} canvas + * @param {Float} radius + * @param {boolean} blurAlphaChannel + * @returns {undefined} + */ +function processImage (img, canvas, radius, blurAlphaChannel) { + if (typeof img === 'string') { + img = document.getElementById(img); + } + if (!img || !('naturalWidth' in img)) { + return; + } + const w = img.naturalWidth; + const h = img.naturalHeight; + + if (typeof canvas === 'string') { + canvas = document.getElementById(canvas); + } + if (!canvas || !('getContext' in canvas)) { + return; + } + + canvas.style.width = w + 'px'; + canvas.style.height = h + 'px'; + canvas.width = w; + canvas.height = h; + + const context = canvas.getContext('2d'); + context.clearRect(0, 0, w, h); + context.drawImage(img, 0, 0); + + if (isNaN(radius) || radius < 1) { return; } + + if (blurAlphaChannel) { + processCanvasRGBA(canvas, 0, 0, w, h, radius); + } else { + processCanvasRGB(canvas, 0, 0, w, h, radius); + } +} + +/** + * @param {string|HTMLCanvasElement} canvas + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @throws {Error} + * @returns {ImageData} See {@link https://html.spec.whatwg.org/multipage/canvas.html#imagedata} + */ +function getImageDataFromCanvas (canvas, topX, topY, width, height) { + if (typeof canvas === 'string') { + canvas = document.getElementById(canvas); + } + if (!canvas || !('getContext' in canvas)) { + return; + } + + const context = canvas.getContext('2d'); + + try { + return context.getImageData(topX, topY, width, height); + } catch (e) { + throw new Error('unable to access image data: ' + e); + } +} + +/** + * @param {HTMLCanvasElement} canvas + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @param {Float} radius + * @returns {undefined} + */ +function processCanvasRGBA (canvas, topX, topY, width, height, radius) { + if (isNaN(radius) || radius < 1) { return; } + radius |= 0; + + let imageData = getImageDataFromCanvas(canvas, topX, topY, width, height); + + imageData = processImageDataRGBA(imageData, topX, topY, width, height, radius); + + canvas.getContext('2d').putImageData(imageData, topX, topY); +} + +/** + * @param {ImageData} imageData + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @param {Float} radius + * @returns {ImageData} + */ +function processImageDataRGBA (imageData, topX, topY, width, height, radius) { + const pixels = imageData.data; + + let x, y, i, p, yp, yi, yw, rSum, gSum, bSum, aSum, + rOutSum, gOutSum, bOutSum, aOutSum, + rInSum, gInSum, bInSum, aInSum, + pr, pg, pb, pa, rbs; + + const div = radius + radius + 1; + // const w4 = width << 2; + const widthMinus1 = width - 1; + const heightMinus1 = height - 1; + const radiusPlus1 = radius + 1; + const sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2; + + const stackStart = new BlurStack(); + let stack = stackStart; + let stackEnd; + for (i = 1; i < div; i++) { + stack = stack.next = new BlurStack(); + if (i === radiusPlus1) { + stackEnd = stack; + } + } + stack.next = stackStart; + let stackIn = null; + let stackOut = null; + + yw = yi = 0; + + const mulSum = mulTable[radius]; + const shgSum = shgTable[radius]; + + for (y = 0; y < height; y++) { + rInSum = gInSum = bInSum = aInSum = rSum = gSum = bSum = aSum = 0; + + rOutSum = radiusPlus1 * (pr = pixels[yi]); + gOutSum = radiusPlus1 * (pg = pixels[yi + 1]); + bOutSum = radiusPlus1 * (pb = pixels[yi + 2]); + aOutSum = radiusPlus1 * (pa = pixels[yi + 3]); + + rSum += sumFactor * pr; + gSum += sumFactor * pg; + bSum += sumFactor * pb; + aSum += sumFactor * pa; + + stack = stackStart; + + for (i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + stack = stack.next; + } + + for (i = 1; i < radiusPlus1; i++) { + p = yi + ((widthMinus1 < i ? widthMinus1 : i) << 2); + rSum += (stack.r = (pr = pixels[p])) * (rbs = radiusPlus1 - i); + gSum += (stack.g = (pg = pixels[p + 1])) * rbs; + bSum += (stack.b = (pb = pixels[p + 2])) * rbs; + aSum += (stack.a = (pa = pixels[p + 3])) * rbs; + + rInSum += pr; + gInSum += pg; + bInSum += pb; + aInSum += pa; + + stack = stack.next; + } + + stackIn = stackStart; + stackOut = stackEnd; + for (x = 0; x < width; x++) { + pixels[yi + 3] = pa = (aSum * mulSum) >> shgSum; + if (pa !== 0) { + pa = 255 / pa; + pixels[yi] = ((rSum * mulSum) >> shgSum) * pa; + pixels[yi + 1] = ((gSum * mulSum) >> shgSum) * pa; + pixels[yi + 2] = ((bSum * mulSum) >> shgSum) * pa; + } else { + pixels[yi] = pixels[yi + 1] = pixels[yi + 2] = 0; + } + + rSum -= rOutSum; + gSum -= gOutSum; + bSum -= bOutSum; + aSum -= aOutSum; + + rOutSum -= stackIn.r; + gOutSum -= stackIn.g; + bOutSum -= stackIn.b; + aOutSum -= stackIn.a; + + p = (yw + ((p = x + radius + 1) < widthMinus1 ? p : widthMinus1)) << 2; + + rInSum += (stackIn.r = pixels[p]); + gInSum += (stackIn.g = pixels[p + 1]); + bInSum += (stackIn.b = pixels[p + 2]); + aInSum += (stackIn.a = pixels[p + 3]); + + rSum += rInSum; + gSum += gInSum; + bSum += bInSum; + aSum += aInSum; + + stackIn = stackIn.next; + + rOutSum += (pr = stackOut.r); + gOutSum += (pg = stackOut.g); + bOutSum += (pb = stackOut.b); + aOutSum += (pa = stackOut.a); + + rInSum -= pr; + gInSum -= pg; + bInSum -= pb; + aInSum -= pa; + + stackOut = stackOut.next; + + yi += 4; + } + yw += width; + } + + for (x = 0; x < width; x++) { + gInSum = bInSum = aInSum = rInSum = gSum = bSum = aSum = rSum = 0; + + yi = x << 2; + rOutSum = radiusPlus1 * (pr = pixels[yi]); + gOutSum = radiusPlus1 * (pg = pixels[yi + 1]); + bOutSum = radiusPlus1 * (pb = pixels[yi + 2]); + aOutSum = radiusPlus1 * (pa = pixels[yi + 3]); + + rSum += sumFactor * pr; + gSum += sumFactor * pg; + bSum += sumFactor * pb; + aSum += sumFactor * pa; + + stack = stackStart; + + for (i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + stack = stack.next; + } + + yp = width; + + for (i = 1; i <= radius; i++) { + yi = (yp + x) << 2; + + rSum += (stack.r = (pr = pixels[yi])) * (rbs = radiusPlus1 - i); + gSum += (stack.g = (pg = pixels[yi + 1])) * rbs; + bSum += (stack.b = (pb = pixels[yi + 2])) * rbs; + aSum += (stack.a = (pa = pixels[yi + 3])) * rbs; + + rInSum += pr; + gInSum += pg; + bInSum += pb; + aInSum += pa; + + stack = stack.next; + + if (i < heightMinus1) { + yp += width; + } + } + + yi = x; + stackIn = stackStart; + stackOut = stackEnd; + for (y = 0; y < height; y++) { + p = yi << 2; + pixels[p + 3] = pa = (aSum * mulSum) >> shgSum; + if (pa > 0) { + pa = 255 / pa; + pixels[p] = ((rSum * mulSum) >> shgSum) * pa; + pixels[p + 1] = ((gSum * mulSum) >> shgSum) * pa; + pixels[p + 2] = ((bSum * mulSum) >> shgSum) * pa; + } else { + pixels[p] = pixels[p + 1] = pixels[p + 2] = 0; + } + + rSum -= rOutSum; + gSum -= gOutSum; + bSum -= bOutSum; + aSum -= aOutSum; + + rOutSum -= stackIn.r; + gOutSum -= stackIn.g; + bOutSum -= stackIn.b; + aOutSum -= stackIn.a; + + p = (x + (((p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1) * width)) << 2; + + rSum += (rInSum += (stackIn.r = pixels[p])); + gSum += (gInSum += (stackIn.g = pixels[p + 1])); + bSum += (bInSum += (stackIn.b = pixels[p + 2])); + aSum += (aInSum += (stackIn.a = pixels[p + 3])); + + stackIn = stackIn.next; + + rOutSum += (pr = stackOut.r); + gOutSum += (pg = stackOut.g); + bOutSum += (pb = stackOut.b); + aOutSum += (pa = stackOut.a); + + rInSum -= pr; + gInSum -= pg; + bInSum -= pb; + aInSum -= pa; + + stackOut = stackOut.next; + + yi += width; + } + } + return imageData; +} + +/** + * @param {HTMLCanvasElement} canvas + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @param {Float} radius + * @returns {undefined} + */ +function processCanvasRGB (canvas, topX, topY, width, height, radius) { + if (isNaN(radius) || radius < 1) { return; } + radius |= 0; + + let imageData = getImageDataFromCanvas(canvas, topX, topY, width, height); + imageData = processImageDataRGB(imageData, topX, topY, width, height, radius); + + canvas.getContext('2d').putImageData(imageData, topX, topY); +} + +/** + * @param {ImageData} imageData + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @param {Float} radius + * @returns {ImageData} + */ +function processImageDataRGB (imageData, topX, topY, width, height, radius) { + const pixels = imageData.data; + + let x, y, i, p, yp, yi, yw, rSum, gSum, bSum, + rOutSum, gOutSum, bOutSum, + rInSum, gInSum, bInSum, + pr, pg, pb, rbs; + + const div = radius + radius + 1; + // const w4 = width << 2; + const widthMinus1 = width - 1; + const heightMinus1 = height - 1; + const radiusPlus1 = radius + 1; + const sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2; + + const stackStart = new BlurStack(); + let stack = stackStart; + let stackEnd; + for (i = 1; i < div; i++) { + stack = stack.next = new BlurStack(); + if (i === radiusPlus1) { + stackEnd = stack; + } + } + stack.next = stackStart; + let stackIn = null; + let stackOut = null; + + yw = yi = 0; + + const mulSum = mulTable[radius]; + const shgSum = shgTable[radius]; + + for (y = 0; y < height; y++) { + rInSum = gInSum = bInSum = rSum = gSum = bSum = 0; + + rOutSum = radiusPlus1 * (pr = pixels[yi]); + gOutSum = radiusPlus1 * (pg = pixels[yi + 1]); + bOutSum = radiusPlus1 * (pb = pixels[yi + 2]); + + rSum += sumFactor * pr; + gSum += sumFactor * pg; + bSum += sumFactor * pb; + + stack = stackStart; + + for (i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack = stack.next; + } + + for (i = 1; i < radiusPlus1; i++) { + p = yi + ((widthMinus1 < i ? widthMinus1 : i) << 2); + rSum += (stack.r = (pr = pixels[p])) * (rbs = radiusPlus1 - i); + gSum += (stack.g = (pg = pixels[p + 1])) * rbs; + bSum += (stack.b = (pb = pixels[p + 2])) * rbs; + + rInSum += pr; + gInSum += pg; + bInSum += pb; + + stack = stack.next; + } + + stackIn = stackStart; + stackOut = stackEnd; + for (x = 0; x < width; x++) { + pixels[yi] = (rSum * mulSum) >> shgSum; + pixels[yi + 1] = (gSum * mulSum) >> shgSum; + pixels[yi + 2] = (bSum * mulSum) >> shgSum; + + rSum -= rOutSum; + gSum -= gOutSum; + bSum -= bOutSum; + + rOutSum -= stackIn.r; + gOutSum -= stackIn.g; + bOutSum -= stackIn.b; + + p = (yw + ((p = x + radius + 1) < widthMinus1 ? p : widthMinus1)) << 2; + + rInSum += (stackIn.r = pixels[p]); + gInSum += (stackIn.g = pixels[p + 1]); + bInSum += (stackIn.b = pixels[p + 2]); + + rSum += rInSum; + gSum += gInSum; + bSum += bInSum; + + stackIn = stackIn.next; + + rOutSum += (pr = stackOut.r); + gOutSum += (pg = stackOut.g); + bOutSum += (pb = stackOut.b); + + rInSum -= pr; + gInSum -= pg; + bInSum -= pb; + + stackOut = stackOut.next; + + yi += 4; + } + yw += width; + } + + for (x = 0; x < width; x++) { + gInSum = bInSum = rInSum = gSum = bSum = rSum = 0; + + yi = x << 2; + rOutSum = radiusPlus1 * (pr = pixels[yi]); + gOutSum = radiusPlus1 * (pg = pixels[yi + 1]); + bOutSum = radiusPlus1 * (pb = pixels[yi + 2]); + + rSum += sumFactor * pr; + gSum += sumFactor * pg; + bSum += sumFactor * pb; + + stack = stackStart; + + for (i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack = stack.next; + } + + yp = width; + + for (i = 1; i <= radius; i++) { + yi = (yp + x) << 2; + + rSum += (stack.r = (pr = pixels[yi])) * (rbs = radiusPlus1 - i); + gSum += (stack.g = (pg = pixels[yi + 1])) * rbs; + bSum += (stack.b = (pb = pixels[yi + 2])) * rbs; + + rInSum += pr; + gInSum += pg; + bInSum += pb; + + stack = stack.next; + + if (i < heightMinus1) { + yp += width; + } + } + + yi = x; + stackIn = stackStart; + stackOut = stackEnd; + for (y = 0; y < height; y++) { + p = yi << 2; + pixels[p] = (rSum * mulSum) >> shgSum; + pixels[p + 1] = (gSum * mulSum) >> shgSum; + pixels[p + 2] = (bSum * mulSum) >> shgSum; + + rSum -= rOutSum; + gSum -= gOutSum; + bSum -= bOutSum; + + rOutSum -= stackIn.r; + gOutSum -= stackIn.g; + bOutSum -= stackIn.b; + + p = (x + (((p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1) * width)) << 2; + + rSum += (rInSum += (stackIn.r = pixels[p])); + gSum += (gInSum += (stackIn.g = pixels[p + 1])); + bSum += (bInSum += (stackIn.b = pixels[p + 2])); + + stackIn = stackIn.next; + + rOutSum += (pr = stackOut.r); + gOutSum += (pg = stackOut.g); + bOutSum += (pb = stackOut.b); + + rInSum -= pr; + gInSum -= pg; + bInSum -= pb; + + stackOut = stackOut.next; + + yi += width; + } + } + + return imageData; +} + +/** + * + */ +export class BlurStack { + constructor () { + this.r = 0; + this.g = 0; + this.b = 0; + this.a = 0; + this.next = null; + } +} + +export { + /** + * @class module:StackBlur.image + * @see module:StackBlur~processImage + */ + processImage as image, + /** + * @class module:StackBlur.canvasRGBA + * @see module:StackBlur~processCanvasRGBA + */ + processCanvasRGBA as canvasRGBA, + /** + * @class module:StackBlur.canvasRGB + * @see module:StackBlur~processCanvasRGB + */ + processCanvasRGB as canvasRGB, + /** + * @class module:StackBlur.imageDataRGBA + * @see module:StackBlur~processImageDataRGBA + */ + processImageDataRGBA as imageDataRGBA, + /** + * @class module:StackBlur.imageDataRGB + * @see module:StackBlur~processImageDataRGB + */ + processImageDataRGB as imageDataRGB +}; diff --git a/editor/canvg/canvg.js b/editor/canvg/canvg.js index f0d6d98c..3a1130ef 100644 --- a/editor/canvg/canvg.js +++ b/editor/canvg/canvg.js @@ -1,2922 +1,2978 @@ -/* - * canvg.js - Javascript SVG parser and renderer on Canvas - * MIT Licensed - * Gabe Lerner (gabelerner@gmail.com) - * http://code.google.com/p/canvg/ - * - * Requires: rgbcolor.js - http://www.phpied.com/rgb-color-parser-in-javascript/ - */ -(function(){ - // canvg(target, s) - // empty parameters: replace all 'svg' elements on page with 'canvas' elements - // target: canvas element or the id of a canvas element - // s: svg string, url to svg file, or xml document - // opts: optional hash of options - // ignoreMouse: true => ignore mouse events - // ignoreAnimation: true => ignore animations - // ignoreDimensions: true => does not try to resize canvas - // ignoreClear: true => does not clear canvas - // offsetX: int => draws at a x offset - // offsetY: int => draws at a y offset - // scaleWidth: int => scales horizontally to width - // scaleHeight: int => scales vertically to height - // renderCallback: function => will call the function after the first render is completed - // forceRedraw: function => will call the function on every frame, if it returns true, will redraw - this.canvg = function (target, s, opts) { - // no parameters - if (target == null && s == null && opts == null) { - var svgTags = document.querySelectorAll('svg'); - for (var i=0; i]*>/, ''); - var xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); - xmlDoc.async = 'false'; - xmlDoc.loadXML(xml); - return xmlDoc; - } - } - - svg.Property = function(name, value) { - this.name = name; - this.value = value; - } - svg.Property.prototype.getValue = function() { - return this.value; - } - - svg.Property.prototype.hasValue = function() { - return (this.value != null && this.value !== ''); - } - - // return the numerical value of the property - svg.Property.prototype.numValue = function() { - if (!this.hasValue()) return 0; - - var n = parseFloat(this.value); - if ((this.value + '').match(/%$/)) { - n = n / 100.0; - } - return n; - } - - svg.Property.prototype.valueOrDefault = function(def) { - if (this.hasValue()) return this.value; - return def; - } - - svg.Property.prototype.numValueOrDefault = function(def) { - if (this.hasValue()) return this.numValue(); - return def; - } - - // color extensions - // augment the current color value with the opacity - svg.Property.prototype.addOpacity = function(opacityProp) { - var newValue = this.value; - if (opacityProp.value != null && opacityProp.value != '' && typeof(this.value)=='string') { // can only add opacity to colors, not patterns - var color = new RGBColor(this.value); - if (color.ok) { - newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacityProp.numValue() + ')'; - } - } - return new svg.Property(this.name, newValue); - } - - // definition extensions - // get the definition from the definitions table - svg.Property.prototype.getDefinition = function() { - var name = this.value.match(/#([^\)'"]+)/); - if (name) { name = name[1]; } - if (!name) { name = this.value; } - return svg.Definitions[name]; - } - - svg.Property.prototype.isUrlDefinition = function() { - return this.value.indexOf('url(') == 0 - } - - svg.Property.prototype.getFillStyleDefinition = function(e, opacityProp) { - var def = this.getDefinition(); - - // gradient - if (def != null && def.createGradient) { - return def.createGradient(svg.ctx, e, opacityProp); - } - - // pattern - if (def != null && def.createPattern) { - if (def.getHrefAttribute().hasValue()) { - var pt = def.attribute('patternTransform'); - def = def.getHrefAttribute().getDefinition(); - if (pt.hasValue()) { def.attribute('patternTransform', true).value = pt.value; } - } - return def.createPattern(svg.ctx, e); - } - - return null; - } - - // length extensions - svg.Property.prototype.getDPI = function(viewPort) { - return 96.0; // TODO: compute? - } - - svg.Property.prototype.getEM = function(viewPort) { - var em = 12; - - var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); - if (fontSize.hasValue()) em = fontSize.toPixels(viewPort); - - return em; - } - - svg.Property.prototype.getUnits = function() { - var s = this.value+''; - return s.replace(/[0-9\.\-]/g,''); - } - - // get the length as pixels - svg.Property.prototype.toPixels = function(viewPort, processPercent) { - if (!this.hasValue()) return 0; - var s = this.value+''; - if (s.match(/em$/)) return this.numValue() * this.getEM(viewPort); - if (s.match(/ex$/)) return this.numValue() * this.getEM(viewPort) / 2.0; - if (s.match(/px$/)) return this.numValue(); - if (s.match(/pt$/)) return this.numValue() * this.getDPI(viewPort) * (1.0 / 72.0); - if (s.match(/pc$/)) return this.numValue() * 15; - if (s.match(/cm$/)) return this.numValue() * this.getDPI(viewPort) / 2.54; - if (s.match(/mm$/)) return this.numValue() * this.getDPI(viewPort) / 25.4; - if (s.match(/in$/)) return this.numValue() * this.getDPI(viewPort); - if (s.match(/%$/)) return this.numValue() * svg.ViewPort.ComputeSize(viewPort); - var n = this.numValue(); - if (processPercent && n < 1.0) return n * svg.ViewPort.ComputeSize(viewPort); - return n; - } - - // time extensions - // get the time as milliseconds - svg.Property.prototype.toMilliseconds = function() { - if (!this.hasValue()) return 0; - var s = this.value+''; - if (s.match(/s$/)) return this.numValue() * 1000; - if (s.match(/ms$/)) return this.numValue(); - return this.numValue(); - } - - // angle extensions - // get the angle as radians - svg.Property.prototype.toRadians = function() { - if (!this.hasValue()) return 0; - var s = this.value+''; - if (s.match(/deg$/)) return this.numValue() * (Math.PI / 180.0); - if (s.match(/grad$/)) return this.numValue() * (Math.PI / 200.0); - if (s.match(/rad$/)) return this.numValue(); - return this.numValue() * (Math.PI / 180.0); - } - - // text extensions - // get the text baseline - var textBaselineMapping = { - 'baseline': 'alphabetic', - 'before-edge': 'top', - 'text-before-edge': 'top', - 'middle': 'middle', - 'central': 'middle', - 'after-edge': 'bottom', - 'text-after-edge': 'bottom', - 'ideographic': 'ideographic', - 'alphabetic': 'alphabetic', - 'hanging': 'hanging', - 'mathematical': 'alphabetic' - }; - svg.Property.prototype.toTextBaseline = function () { - if (!this.hasValue()) return null; - return textBaselineMapping[this.value]; - } - - // fonts - svg.Font = new (function() { - this.Styles = 'normal|italic|oblique|inherit'; - this.Variants = 'normal|small-caps|inherit'; - this.Weights = 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit'; - - this.CreateFont = function(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) { - var f = inherit != null ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font); - return { - fontFamily: fontFamily || f.fontFamily, - fontSize: fontSize || f.fontSize, - fontStyle: fontStyle || f.fontStyle, - fontWeight: fontWeight || f.fontWeight, - fontVariant: fontVariant || f.fontVariant, - toString: function () { return [this.fontStyle, this.fontVariant, this.fontWeight, this.fontSize, this.fontFamily].join(' ') } - } - } - - var that = this; - this.Parse = function(s) { - var f = {}; - var d = svg.trim(svg.compressSpaces(s || '')).split(' '); - var set = { fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false } - var ff = ''; - for (var i=0; i this.x2) this.x2 = x; - } - - if (y != null) { - if (isNaN(this.y1) || isNaN(this.y2)) { - this.y1 = y; - this.y2 = y; - } - if (y < this.y1) this.y1 = y; - if (y > this.y2) this.y2 = y; - } - } - this.addX = function(x) { this.addPoint(x, null); } - this.addY = function(y) { this.addPoint(null, y); } - - this.addBoundingBox = function(bb) { - this.addPoint(bb.x1, bb.y1); - this.addPoint(bb.x2, bb.y2); - } - - this.addQuadraticCurve = function(p0x, p0y, p1x, p1y, p2x, p2y) { - var cp1x = p0x + 2/3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) - var cp1y = p0y + 2/3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) - var cp2x = cp1x + 1/3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) - var cp2y = cp1y + 1/3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) - this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); - } - - this.addBezierCurve = function(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { - // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html - var p0 = [p0x, p0y], p1 = [p1x, p1y], p2 = [p2x, p2y], p3 = [p3x, p3y]; - this.addPoint(p0[0], p0[1]); - this.addPoint(p3[0], p3[1]); - - for (i=0; i<=1; i++) { - var f = function(t) { - return Math.pow(1-t, 3) * p0[i] - + 3 * Math.pow(1-t, 2) * t * p1[i] - + 3 * (1-t) * Math.pow(t, 2) * p2[i] - + Math.pow(t, 3) * p3[i]; - } - - var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; - var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; - var c = 3 * p1[i] - 3 * p0[i]; - - if (a == 0) { - if (b == 0) continue; - var t = -c / b; - if (0 < t && t < 1) { - if (i == 0) this.addX(f(t)); - if (i == 1) this.addY(f(t)); - } - continue; - } - - var b2ac = Math.pow(b, 2) - 4 * c * a; - if (b2ac < 0) continue; - var t1 = (-b + Math.sqrt(b2ac)) / (2 * a); - if (0 < t1 && t1 < 1) { - if (i == 0) this.addX(f(t1)); - if (i == 1) this.addY(f(t1)); - } - var t2 = (-b - Math.sqrt(b2ac)) / (2 * a); - if (0 < t2 && t2 < 1) { - if (i == 0) this.addX(f(t2)); - if (i == 1) this.addY(f(t2)); - } - } - } - - this.isPointInBox = function(x, y) { - return (this.x1 <= x && x <= this.x2 && this.y1 <= y && y <= this.y2); - } - - this.addPoint(x1, y1); - this.addPoint(x2, y2); - } - - // transforms - svg.Transform = function(v) { - var that = this; - this.Type = {} - - // translate - this.Type.translate = function(s) { - this.p = svg.CreatePoint(s); - this.apply = function(ctx) { - ctx.translate(this.p.x || 0.0, this.p.y || 0.0); - } - this.unapply = function(ctx) { - ctx.translate(-1.0 * this.p.x || 0.0, -1.0 * this.p.y || 0.0); - } - this.applyToPoint = function(p) { - p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); - } - } - - // rotate - this.Type.rotate = function(s) { - var a = svg.ToNumberArray(s); - this.angle = new svg.Property('angle', a[0]); - this.cx = a[1] || 0; - this.cy = a[2] || 0; - this.apply = function(ctx) { - ctx.translate(this.cx, this.cy); - ctx.rotate(this.angle.toRadians()); - ctx.translate(-this.cx, -this.cy); - } - this.unapply = function(ctx) { - ctx.translate(this.cx, this.cy); - ctx.rotate(-1.0 * this.angle.toRadians()); - ctx.translate(-this.cx, -this.cy); - } - this.applyToPoint = function(p) { - var a = this.angle.toRadians(); - p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); - p.applyTransform([Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0]); - p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]); - } - } - - this.Type.scale = function(s) { - this.p = svg.CreatePoint(s); - this.apply = function(ctx) { - ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0); - } - this.unapply = function(ctx) { - ctx.scale(1.0 / this.p.x || 1.0, 1.0 / this.p.y || this.p.x || 1.0); - } - this.applyToPoint = function(p) { - p.applyTransform([this.p.x || 0.0, 0, 0, this.p.y || 0.0, 0, 0]); - } - } - - this.Type.matrix = function(s) { - this.m = svg.ToNumberArray(s); - this.apply = function(ctx) { - ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]); - } - this.applyToPoint = function(p) { - p.applyTransform(this.m); - } - } - - this.Type.SkewBase = function(s) { - this.base = that.Type.matrix; - this.base(s); - this.angle = new svg.Property('angle', s); - } - this.Type.SkewBase.prototype = new this.Type.matrix; - - this.Type.skewX = function(s) { - this.base = that.Type.SkewBase; - this.base(s); - this.m = [1, 0, Math.tan(this.angle.toRadians()), 1, 0, 0]; - } - this.Type.skewX.prototype = new this.Type.SkewBase; - - this.Type.skewY = function(s) { - this.base = that.Type.SkewBase; - this.base(s); - this.m = [1, Math.tan(this.angle.toRadians()), 0, 1, 0, 0]; - } - this.Type.skewY.prototype = new this.Type.SkewBase; - - this.transforms = []; - - this.apply = function(ctx) { - for (var i=0; i=0; i--) { - this.transforms[i].unapply(ctx); - } - } - - this.applyToPoint = function(p) { - for (var i=0; i= this.tokens.length - 1; - } - - this.isCommandOrEnd = function() { - if (this.isEnd()) return true; - return this.tokens[this.i + 1].match(/^[A-Za-z]$/) != null; - } - - this.isRelativeCommand = function() { - switch(this.command) - { - case 'm': - case 'l': - case 'h': - case 'v': - case 'c': - case 's': - case 'q': - case 't': - case 'a': - case 'z': - return true; - break; - } - return false; - } - - this.getToken = function() { - this.i++; - return this.tokens[this.i]; - } - - this.getScalar = function() { - return parseFloat(this.getToken()); - } - - this.nextCommand = function() { - this.previousCommand = this.command; - this.command = this.getToken(); - } - - this.getPoint = function() { - var p = new svg.Point(this.getScalar(), this.getScalar()); - return this.makeAbsolute(p); - } - - this.getAsControlPoint = function() { - var p = this.getPoint(); - this.control = p; - return p; - } - - this.getAsCurrentPoint = function() { - var p = this.getPoint(); - this.current = p; - return p; - } - - this.getReflectedControlPoint = function() { - if (this.previousCommand.toLowerCase() != 'c' && - this.previousCommand.toLowerCase() != 's' && - this.previousCommand.toLowerCase() != 'q' && - this.previousCommand.toLowerCase() != 't' ){ - return this.current; - } - - // reflect point - var p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y); - return p; - } - - this.makeAbsolute = function(p) { - if (this.isRelativeCommand()) { - p.x += this.current.x; - p.y += this.current.y; - } - return p; - } - - this.addMarker = function(p, from, priorTo) { - // if the last angle isn't filled in because we didn't have this point yet ... - if (priorTo != null && this.angles.length > 0 && this.angles[this.angles.length-1] == null) { - this.angles[this.angles.length-1] = this.points[this.points.length-1].angleTo(priorTo); - } - this.addMarkerAngle(p, from == null ? null : from.angleTo(p)); - } - - this.addMarkerAngle = function(p, a) { - this.points.push(p); - this.angles.push(a); - } - - this.getMarkerPoints = function() { return this.points; } - this.getMarkerAngles = function() { - for (var i=0; i 1) { - rx *= Math.sqrt(l); - ry *= Math.sqrt(l); - } - // cx', cy' - var s = (largeArcFlag == sweepFlag ? -1 : 1) * Math.sqrt( - ((Math.pow(rx,2)*Math.pow(ry,2))-(Math.pow(rx,2)*Math.pow(currp.y,2))-(Math.pow(ry,2)*Math.pow(currp.x,2))) / - (Math.pow(rx,2)*Math.pow(currp.y,2)+Math.pow(ry,2)*Math.pow(currp.x,2)) - ); - if (isNaN(s)) s = 0; - var cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); - // cx, cy - var centp = new svg.Point( - (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, - (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y - ); - // vector magnitude - var m = function(v) { return Math.sqrt(Math.pow(v[0],2) + Math.pow(v[1],2)); } - // ratio between two vectors - var r = function(u, v) { return (u[0]*v[0]+u[1]*v[1]) / (m(u)*m(v)) } - // angle between two vectors - var a = function(u, v) { return (u[0]*v[1] < u[1]*v[0] ? -1 : 1) * Math.acos(r(u,v)); } - // initial angle - var a1 = a([1,0], [(currp.x-cpp.x)/rx,(currp.y-cpp.y)/ry]); - // angle delta - var u = [(currp.x-cpp.x)/rx,(currp.y-cpp.y)/ry]; - var v = [(-currp.x-cpp.x)/rx,(-currp.y-cpp.y)/ry]; - var ad = a(u, v); - if (r(u,v) <= -1) ad = Math.PI; - if (r(u,v) >= 1) ad = 0; - - // for markers - var dir = 1 - sweepFlag ? 1.0 : -1.0; - var ah = a1 + dir * (ad / 2.0); - var halfWay = new svg.Point( - centp.x + rx * Math.cos(ah), - centp.y + ry * Math.sin(ah) - ); - pp.addMarkerAngle(halfWay, ah - dir * Math.PI / 2); - pp.addMarkerAngle(cp, ah - dir * Math.PI); - - bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better - if (ctx != null) { - var r = rx > ry ? rx : ry; - var sx = rx > ry ? 1 : rx / ry; - var sy = rx > ry ? ry / rx : 1; - - ctx.translate(centp.x, centp.y); - ctx.rotate(xAxisRotation); - ctx.scale(sx, sy); - ctx.arc(0, 0, r, a1, a1 + ad, 1 - sweepFlag); - ctx.scale(1/sx, 1/sy); - ctx.rotate(-xAxisRotation); - ctx.translate(-centp.x, -centp.y); - } - } - break; - case 'Z': - case 'z': - if (ctx != null) ctx.closePath(); - pp.current = pp.start; - } - } - - return bb; - } - - this.getMarkers = function() { - var points = this.PathParser.getMarkerPoints(); - var angles = this.PathParser.getMarkerAngles(); - - var markers = []; - for (var i=0; i 1) this.offset = 1; - - var stopColor = this.style('stop-color'); - if (this.style('stop-opacity').hasValue()) stopColor = stopColor.addOpacity(this.style('stop-opacity')); - this.color = stopColor.value; - } - svg.Element.stop.prototype = new svg.Element.ElementBase; - - // animation base element - svg.Element.AnimateBase = function(node) { - this.base = svg.Element.ElementBase; - this.base(node); - - svg.Animations.push(this); - - this.duration = 0.0; - this.begin = this.attribute('begin').toMilliseconds(); - this.maxDuration = this.begin + this.attribute('dur').toMilliseconds(); - - this.getProperty = function() { - var attributeType = this.attribute('attributeType').value; - var attributeName = this.attribute('attributeName').value; - - if (attributeType == 'CSS') { - return this.parent.style(attributeName, true); - } - return this.parent.attribute(attributeName, true); - }; - - this.initialValue = null; - this.initialUnits = ''; - this.removed = false; - - this.calcValue = function() { - // OVERRIDE ME! - return ''; - } - - this.update = function(delta) { - // set initial value - if (this.initialValue == null) { - this.initialValue = this.getProperty().value; - this.initialUnits = this.getProperty().getUnits(); - } - - // if we're past the end time - if (this.duration > this.maxDuration) { - // loop for indefinitely repeating animations - if (this.attribute('repeatCount').value == 'indefinite' - || this.attribute('repeatDur').value == 'indefinite') { - this.duration = 0.0 - } - else if (this.attribute('fill').valueOrDefault('remove') == 'freeze' && !this.frozen) { - this.frozen = true; - this.parent.animationFrozen = true; - this.parent.animationFrozenValue = this.getProperty().value; - } - else if (this.attribute('fill').valueOrDefault('remove') == 'remove' && !this.removed) { - this.removed = true; - this.getProperty().value = this.parent.animationFrozen ? this.parent.animationFrozenValue : this.initialValue; - return true; - } - return false; - } - this.duration = this.duration + delta; - - // if we're past the begin time - var updated = false; - if (this.begin < this.duration) { - var newValue = this.calcValue(); // tween - - if (this.attribute('type').hasValue()) { - // for transform, etc. - var type = this.attribute('type').value; - newValue = type + '(' + newValue + ')'; - } - - this.getProperty().value = newValue; - updated = true; - } - - return updated; - } - - this.from = this.attribute('from'); - this.to = this.attribute('to'); - this.values = this.attribute('values'); - if (this.values.hasValue()) this.values.value = this.values.value.split(';'); - - // fraction of duration we've covered - this.progress = function() { - var ret = { progress: (this.duration - this.begin) / (this.maxDuration - this.begin) }; - if (this.values.hasValue()) { - var p = ret.progress * (this.values.value.length - 1); - var lb = Math.floor(p), ub = Math.ceil(p); - ret.from = new svg.Property('from', parseFloat(this.values.value[lb])); - ret.to = new svg.Property('to', parseFloat(this.values.value[ub])); - ret.progress = (p - lb) / (ub - lb); - } - else { - ret.from = this.from; - ret.to = this.to; - } - return ret; - } - } - svg.Element.AnimateBase.prototype = new svg.Element.ElementBase; - - // animate element - svg.Element.animate = function(node) { - this.base = svg.Element.AnimateBase; - this.base(node); - - this.calcValue = function() { - var p = this.progress(); - - // tween value linearly - var newValue = p.from.numValue() + (p.to.numValue() - p.from.numValue()) * p.progress; - return newValue + this.initialUnits; - }; - } - svg.Element.animate.prototype = new svg.Element.AnimateBase; - - // animate color element - svg.Element.animateColor = function(node) { - this.base = svg.Element.AnimateBase; - this.base(node); - - this.calcValue = function() { - var p = this.progress(); - var from = new RGBColor(p.from.value); - var to = new RGBColor(p.to.value); - - if (from.ok && to.ok) { - // tween color linearly - var r = from.r + (to.r - from.r) * p.progress; - var g = from.g + (to.g - from.g) * p.progress; - var b = from.b + (to.b - from.b) * p.progress; - return 'rgb('+parseInt(r,10)+','+parseInt(g,10)+','+parseInt(b,10)+')'; - } - return this.attribute('from').value; - }; - } - svg.Element.animateColor.prototype = new svg.Element.AnimateBase; - - // animate transform element - svg.Element.animateTransform = function(node) { - this.base = svg.Element.AnimateBase; - this.base(node); - - this.calcValue = function() { - var p = this.progress(); - - // tween value linearly - var from = svg.ToNumberArray(p.from.value); - var to = svg.ToNumberArray(p.to.value); - var newValue = ''; - for (var i=0; i startI && child.attribute('x').hasValue()) break; // new group - width += child.measureTextRecursive(ctx); - } - return -1 * (textAnchor == 'end' ? width : width / 2.0); - } - return 0; - } - - this.renderChild = function(ctx, parent, i) { - var child = parent.children[i]; - if (child.attribute('x').hasValue()) { - child.x = child.attribute('x').toPixels('x') + this.getAnchorDelta(ctx, parent, i); - if (child.attribute('dx').hasValue()) child.x += child.attribute('dx').toPixels('x'); - } - else { - if (this.attribute('dx').hasValue()) this.x += this.attribute('dx').toPixels('x'); - if (child.attribute('dx').hasValue()) this.x += child.attribute('dx').toPixels('x'); - child.x = this.x; - } - this.x = child.x + child.measureText(ctx); - - if (child.attribute('y').hasValue()) { - child.y = child.attribute('y').toPixels('y'); - if (child.attribute('dy').hasValue()) child.y += child.attribute('dy').toPixels('y'); - } - else { - if (this.attribute('dy').hasValue()) this.y += this.attribute('dy').toPixels('y'); - if (child.attribute('dy').hasValue()) this.y += child.attribute('dy').toPixels('y'); - child.y = this.y; - } - this.y = child.y; - - child.render(ctx); - - for (var i=0; i0 && text[i-1]!=' ' && i0 && text[i-1]!=' ' && (i == text.length-1 || text[i+1]==' ')) arabicForm = 'initial'; - if (typeof(font.glyphs[c]) != 'undefined') { - glyph = font.glyphs[c][arabicForm]; - if (glyph == null && font.glyphs[c].type == 'glyph') glyph = font.glyphs[c]; - } - } - else { - glyph = font.glyphs[c]; - } - if (glyph == null) glyph = font.missingGlyph; - return glyph; - } - - this.renderChildren = function(ctx) { - var customFont = this.parent.style('font-family').getDefinition(); - if (customFont != null) { - var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); - var fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle); - var text = this.getText(); - if (customFont.isRTL) text = text.split("").reverse().join(""); - - var dx = svg.ToNumberArray(this.parent.attribute('dx').value); - for (var i=0; i 0) { - var urlStart = srcs[s].indexOf('url'); - var urlEnd = srcs[s].indexOf(')', urlStart); - var url = srcs[s].substr(urlStart + 5, urlEnd - urlStart - 6); - var doc = svg.parseXml(svg.ajax(url)); - var fonts = doc.getElementsByTagName('font'); - for (var f=0; f + * @see https://github.com/canvg/canvg + */ + +import RGBColor from './rgbcolor.js'; +import {canvasRGBA} from './StackBlur.js'; + +let canvasRGBA_ = canvasRGBA; + +/** +* @callback module:canvg.StackBlurCanvasRGBA +* @param {string} id +* @param {Float} x +* @param {Float} y +* @param {Float} width +* @param {Float} height +* @param {Float} blurRadius +*/ + +/** +* @callback module:canvg.ForceRedraw +* @returns {boolean} +*/ + +/** +* @function module:canvg.setStackBlurCanvasRGBA +* @param {module:canvg.StackBlurCanvasRGBA} cb Will be passed the canvas ID, x, y, width, height, blurRadius +*/ +export const setStackBlurCanvasRGBA = (cb) => { + canvasRGBA_ = cb; +}; + +/** +* @typedef {PlainObject} module:canvg.CanvgOptions +* @property {boolean} opts.ignoreMouse true => ignore mouse events +* @property {boolean} opts.ignoreAnimation true => ignore animations +* @property {boolean} opts.ignoreDimensions true => does not try to resize canvas +* @property {boolean} opts.ignoreClear true => does not clear canvas +* @property {Integer} opts.offsetX int => draws at a x offset +* @property {Integer} opts.offsetY int => draws at a y offset +* @property {Integer} opts.scaleWidth int => scales horizontally to width +* @property {Integer} opts.scaleHeight int => scales vertically to height +* @property {module:canvg.ForceRedraw} opts.forceRedraw function => will call the function on every frame, if it returns true, will redraw +* @property {boolean} opts.log Adds log function +* @property {boolean} opts.useCORS Whether to set CORS `crossOrigin` for the image to `Anonymous` +*/ + +/** +* If called with no arguments, it will replace all `` elements on the page with `` elements +* @function module:canvg.canvg +* @param {HTMLCanvasElement|string} target canvas element or the id of a canvas element +* @param {string|XMLDocument} s: svg string, url to svg file, or xml document +* @param {module:canvg.CanvgOptions} [opts] Optional hash of options +* @returns {Promise} All the function after the first render is completed with dom +*/ +export const canvg = function (target, s, opts) { + // no parameters + if (target == null && s == null && opts == null) { + const svgTags = document.querySelectorAll('svg'); + return Promise.all([...svgTags].map((svgTag) => { + const c = document.createElement('canvas'); + c.width = svgTag.clientWidth; + c.height = svgTag.clientHeight; + svgTag.before(c); + svgTag.remove(); + const div = document.createElement('div'); + div.append(svgTag); + return canvg(c, div.innerHTML); + })); + } + + if (typeof target === 'string') { + target = document.getElementById(target); + } + + // store class on canvas + if (target.svg != null) target.svg.stop(); + const svg = build(opts || {}); + // on i.e. 8 for flash canvas, we can't assign the property so check for it + if (!(target.childNodes.length === 1 && target.childNodes[0].nodeName === 'OBJECT')) { + target.svg = svg; + } + + const ctx = target.getContext('2d'); + if (typeof s.documentElement !== 'undefined') { + // load from xml doc + return svg.loadXmlDoc(ctx, s); + } + if (s.substr(0, 1) === '<') { + // load from xml string + return svg.loadXml(ctx, s); + } + // load from url + return svg.load(ctx, s); +}; + +/** +* @param {module:canvg.CanvgOptions} opts +* @returns {object} +* @todo Flesh out exactly what object is returned here (after updating to latest and reincluding our changes here and those of StackBlur) +*/ +function build (opts) { + const svg = {opts}; + + svg.FRAMERATE = 30; + svg.MAX_VIRTUAL_PIXELS = 30000; + + svg.log = function (msg) {}; + if (svg.opts.log === true && typeof console !== 'undefined') { + svg.log = function (msg) { console.log(msg); }; + } + + // globals + svg.init = function (ctx) { + let uniqueId = 0; + svg.UniqueId = function () { + uniqueId++; + return 'canvg' + uniqueId; + }; + svg.Definitions = {}; + svg.Styles = {}; + svg.Animations = []; + svg.Images = []; + svg.ctx = ctx; + svg.ViewPort = { + viewPorts: [], + Clear () { this.viewPorts = []; }, + SetCurrent (width, height) { this.viewPorts.push({ width, height }); }, + RemoveCurrent () { this.viewPorts.pop(); }, + Current () { return this.viewPorts[this.viewPorts.length - 1]; }, + width () { return this.Current().width; }, + height () { return this.Current().height; }, + ComputeSize (d) { + if (d != null && typeof d === 'number') return d; + if (d === 'x') return this.width(); + if (d === 'y') return this.height(); + return Math.sqrt( + Math.pow(this.width(), 2) + Math.pow(this.height(), 2) + ) / Math.sqrt(2); + } + }; + }; + svg.init(); + + // images loaded + svg.ImagesLoaded = function () { + return svg.Images.every((img) => img.loaded); + }; + + // trim + svg.trim = function (s) { + return s.replace(/^\s+|\s+$/g, ''); + }; + + // compress spaces + svg.compressSpaces = function (s) { + return s.replace(/[\s\r\t\n]+/gm, ' '); + }; + + // ajax + svg.ajax = function (url, asynch) { + const AJAX = window.XMLHttpRequest + ? new XMLHttpRequest() + : new window.ActiveXObject('Microsoft.XMLHTTP'); + if (!AJAX) { + return null; + } + if (asynch) { + return new Promise((resolve, reject) => { + const req = AJAX.open('GET', url, true); + req.addEventListener('load', () => { + resolve(AJAX.responseText); + }); + AJAX.send(null); + }); + } + + AJAX.open('GET', url, false); + AJAX.send(null); + return AJAX.responseText; + }; + + // parse xml + svg.parseXml = function (xml) { + if (window.DOMParser) { + const parser = new DOMParser(); + return parser.parseFromString(xml, 'text/xml'); + } else { + xml = xml.replace(/]*>/, ''); + const xmlDoc = new window.ActiveXObject('Microsoft.XMLDOM'); + xmlDoc.async = 'false'; + xmlDoc.loadXML(xml); + return xmlDoc; + } + }; + + // text extensions + // get the text baseline + const textBaselineMapping = { + baseline: 'alphabetic', + 'before-edge': 'top', + 'text-before-edge': 'top', + middle: 'middle', + central: 'middle', + 'after-edge': 'bottom', + 'text-after-edge': 'bottom', + ideographic: 'ideographic', + alphabetic: 'alphabetic', + hanging: 'hanging', + mathematical: 'alphabetic' + }; + + svg.Property = class Property { + constructor (name, value) { + this.name = name; + this.value = value; + } + + getValue () { + return this.value; + } + + hasValue () { + return (this.value != null && this.value !== ''); + } + + // return the numerical value of the property + numValue () { + if (!this.hasValue()) return 0; + + let n = parseFloat(this.value); + if ((this.value + '').match(/%$/)) { + n = n / 100.0; + } + return n; + } + + valueOrDefault (def) { + if (this.hasValue()) return this.value; + return def; + } + + numValueOrDefault (def) { + if (this.hasValue()) return this.numValue(); + return def; + } + + // color extensions + // augment the current color value with the opacity + addOpacity (opacityProp) { + let newValue = this.value; + if (opacityProp.value != null && opacityProp.value !== '' && typeof this.value === 'string') { // can only add opacity to colors, not patterns + const color = new RGBColor(this.value); + if (color.ok) { + newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacityProp.numValue() + ')'; + } + } + return new svg.Property(this.name, newValue); + } + + // definition extensions + // get the definition from the definitions table + getDefinition () { + let name = this.value.match(/#([^)'"]+)/); + if (name) { name = name[1]; } + if (!name) { name = this.value; } + return svg.Definitions[name]; + } + + isUrlDefinition () { + return this.value.startsWith('url('); + } + + getFillStyleDefinition (e, opacityProp) { + let def = this.getDefinition(); + + // gradient + if (def != null && def.createGradient) { + return def.createGradient(svg.ctx, e, opacityProp); + } + + // pattern + if (def != null && def.createPattern) { + if (def.getHrefAttribute().hasValue()) { + const pt = def.attribute('patternTransform'); + def = def.getHrefAttribute().getDefinition(); + if (pt.hasValue()) { def.attribute('patternTransform', true).value = pt.value; } + } + return def.createPattern(svg.ctx, e); + } + + return null; + } + + // length extensions + getDPI (viewPort) { + return 96.0; // TODO: compute? + } + + getEM (viewPort) { + let em = 12; + + const fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + if (fontSize.hasValue()) em = fontSize.toPixels(viewPort); + + return em; + } + + getUnits () { + const s = this.value + ''; + return s.replace(/[0-9.-]/g, ''); + } + + // get the length as pixels + toPixels (viewPort, processPercent) { + if (!this.hasValue()) return 0; + const s = this.value + ''; + if (s.match(/em$/)) return this.numValue() * this.getEM(viewPort); + if (s.match(/ex$/)) return this.numValue() * this.getEM(viewPort) / 2.0; + if (s.match(/px$/)) return this.numValue(); + if (s.match(/pt$/)) return this.numValue() * this.getDPI(viewPort) * (1.0 / 72.0); + if (s.match(/pc$/)) return this.numValue() * 15; + if (s.match(/cm$/)) return this.numValue() * this.getDPI(viewPort) / 2.54; + if (s.match(/mm$/)) return this.numValue() * this.getDPI(viewPort) / 25.4; + if (s.match(/in$/)) return this.numValue() * this.getDPI(viewPort); + if (s.match(/%$/)) return this.numValue() * svg.ViewPort.ComputeSize(viewPort); + const n = this.numValue(); + if (processPercent && n < 1.0) return n * svg.ViewPort.ComputeSize(viewPort); + return n; + } + + // time extensions + // get the time as milliseconds + toMilliseconds () { + if (!this.hasValue()) return 0; + const s = this.value + ''; + if (s.match(/s$/)) return this.numValue() * 1000; + if (s.match(/ms$/)) return this.numValue(); + return this.numValue(); + } + + // angle extensions + // get the angle as radians + toRadians () { + if (!this.hasValue()) return 0; + const s = this.value + ''; + if (s.match(/deg$/)) return this.numValue() * (Math.PI / 180.0); + if (s.match(/grad$/)) return this.numValue() * (Math.PI / 200.0); + if (s.match(/rad$/)) return this.numValue(); + return this.numValue() * (Math.PI / 180.0); + } + + toTextBaseline () { + if (!this.hasValue()) return null; + return textBaselineMapping[this.value]; + } + }; + + // fonts + svg.Font = { + Styles: 'normal|italic|oblique|inherit', + Variants: 'normal|small-caps|inherit', + Weights: 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit', + + CreateFont (fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) { + const f = inherit != null ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font); + return { + fontFamily: fontFamily || f.fontFamily, + fontSize: fontSize || f.fontSize, + fontStyle: fontStyle || f.fontStyle, + fontWeight: fontWeight || f.fontWeight, + fontVariant: fontVariant || f.fontVariant, + toString () { + return [ + this.fontStyle, this.fontVariant, this.fontWeight, + this.fontSize, this.fontFamily + ].join(' '); + } + }; + }, + + Parse (s) { + const f = {}; + const d = svg.trim(svg.compressSpaces(s || '')).split(' '); + const set = { + fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false + }; + let ff = ''; + d.forEach((d) => { + if (!set.fontStyle && this.Styles.includes(d)) { + if (d !== 'inherit') { + f.fontStyle = d; + } + set.fontStyle = true; + } else if (!set.fontVariant && this.Variants.includes(d)) { + if (d !== 'inherit') { + f.fontVariant = d; + } + set.fontStyle = set.fontVariant = true; + } else if (!set.fontWeight && this.Weights.includes(d)) { + if (d !== 'inherit') { + f.fontWeight = d; + } + set.fontStyle = set.fontVariant = set.fontWeight = true; + } else if (!set.fontSize) { + if (d !== 'inherit') { + f.fontSize = d.split('/')[0]; + } + set.fontStyle = set.fontVariant = set.fontWeight = set.fontSize = true; + } else { + if (d !== 'inherit') { ff += d; } + } + }); + if (ff !== '') { f.fontFamily = ff; } + return f; + } + }; + + // points and paths + svg.ToNumberArray = function (s) { + const a = svg.trim(svg.compressSpaces((s || '').replace(/,/g, ' '))).split(' '); + return a.map((a) => parseFloat(a)); + }; + svg.Point = class { + constructor (x, y) { + this.x = x; + this.y = y; + } + + angleTo (p) { + return Math.atan2(p.y - this.y, p.x - this.x); + } + + applyTransform (v) { + const xp = this.x * v[0] + this.y * v[2] + v[4]; + const yp = this.x * v[1] + this.y * v[3] + v[5]; + this.x = xp; + this.y = yp; + } + }; + + svg.CreatePoint = function (s) { + const a = svg.ToNumberArray(s); + return new svg.Point(a[0], a[1]); + }; + svg.CreatePath = function (s) { + const a = svg.ToNumberArray(s); + const path = []; + for (let i = 0; i < a.length; i += 2) { + path.push(new svg.Point(a[i], a[i + 1])); + } + return path; + }; + + // bounding box + svg.BoundingBox = class { + constructor (x1, y1, x2, y2) { // pass in initial points if you want + this.x1 = Number.NaN; + this.y1 = Number.NaN; + this.x2 = Number.NaN; + this.y2 = Number.NaN; + this.addPoint(x1, y1); + this.addPoint(x2, y2); + } + + x () { return this.x1; } + y () { return this.y1; } + width () { return this.x2 - this.x1; } + height () { return this.y2 - this.y1; } + + addPoint (x, y) { + if (x != null) { + if (isNaN(this.x1) || isNaN(this.x2)) { + this.x1 = x; + this.x2 = x; + } + if (x < this.x1) this.x1 = x; + if (x > this.x2) this.x2 = x; + } + + if (y != null) { + if (isNaN(this.y1) || isNaN(this.y2)) { + this.y1 = y; + this.y2 = y; + } + if (y < this.y1) this.y1 = y; + if (y > this.y2) this.y2 = y; + } + } + addX (x) { this.addPoint(x, null); } + addY (y) { this.addPoint(null, y); } + + addBoundingBox (bb) { + this.addPoint(bb.x1, bb.y1); + this.addPoint(bb.x2, bb.y2); + } + + addQuadraticCurve (p0x, p0y, p1x, p1y, p2x, p2y) { + const cp1x = p0x + 2 / 3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) + const cp1y = p0y + 2 / 3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) + const cp2x = cp1x + 1 / 3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) + const cp2y = cp1y + 1 / 3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) + this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); + } + + addBezierCurve (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { + // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html + const p0 = [p0x, p0y], p1 = [p1x, p1y], p2 = [p2x, p2y], p3 = [p3x, p3y]; + this.addPoint(p0[0], p0[1]); + this.addPoint(p3[0], p3[1]); + + for (let i = 0; i <= 1; i++) { + const f = function (t) { + return Math.pow(1 - t, 3) * p0[i] + + 3 * Math.pow(1 - t, 2) * t * p1[i] + + 3 * (1 - t) * Math.pow(t, 2) * p2[i] + + Math.pow(t, 3) * p3[i]; + }; + + const b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; + const a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; + const c = 3 * p1[i] - 3 * p0[i]; + + if (a === 0) { + if (b === 0) continue; + const t = -c / b; + if (t > 0 && t < 1) { + if (i === 0) this.addX(f(t)); + if (i === 1) this.addY(f(t)); + } + continue; + } + + const b2ac = Math.pow(b, 2) - 4 * c * a; + if (b2ac < 0) continue; + const t1 = (-b + Math.sqrt(b2ac)) / (2 * a); + if (t1 > 0 && t1 < 1) { + if (i === 0) this.addX(f(t1)); + if (i === 1) this.addY(f(t1)); + } + const t2 = (-b - Math.sqrt(b2ac)) / (2 * a); + if (t2 > 0 && t2 < 1) { + if (i === 0) this.addX(f(t2)); + if (i === 1) this.addY(f(t2)); + } + } + } + + isPointInBox (x, y) { + return (this.x1 <= x && x <= this.x2 && this.y1 <= y && y <= this.y2); + } + }; + + // transforms + svg.Transform = class { + constructor (v) { + this.Type = { + translate: class { + constructor (s) { + this.p = svg.CreatePoint(s); + this.apply = function (ctx) { + ctx.translate(this.p.x || 0.0, this.p.y || 0.0); + }; + this.unapply = function (ctx) { + ctx.translate(-1.0 * this.p.x || 0.0, -1.0 * this.p.y || 0.0); + }; + this.applyToPoint = function (p) { + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + }; + } + }, + rotate: class { + constructor (s) { + const a = svg.ToNumberArray(s); + this.angle = new svg.Property('angle', a[0]); + this.cx = a[1] || 0; + this.cy = a[2] || 0; + this.apply = function (ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(this.angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + }; + this.unapply = function (ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(-1.0 * this.angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + }; + this.applyToPoint = function (p) { + const a = this.angle.toRadians(); + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + p.applyTransform([Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0]); + p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]); + }; + } + }, + scale: class { + constructor (s) { + this.p = svg.CreatePoint(s); + this.apply = function (ctx) { + ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0); + }; + this.unapply = function (ctx) { + ctx.scale(1.0 / this.p.x || 1.0, 1.0 / this.p.y || this.p.x || 1.0); + }; + this.applyToPoint = function (p) { + p.applyTransform([this.p.x || 0.0, 0, 0, this.p.y || 0.0, 0, 0]); + }; + } + }, + matrix: class { + constructor (s) { + this.m = svg.ToNumberArray(s); + this.apply = function (ctx) { + ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]); + }; + this.applyToPoint = function (p) { + p.applyTransform(this.m); + }; + } + } + }; + Object.assign(this.Type, { + SkewBase: class extends this.Type.matrix { + constructor (s) { + super(s); + this.angle = new svg.Property('angle', s); + } + } + }); + Object.assign(this.Type, { + skewX: class extends this.Type.SkewBase { + constructor (s) { + super(s); + this.m = [1, 0, Math.tan(this.angle.toRadians()), 1, 0, 0]; + } + }, + skewY: class extends this.Type.SkewBase { + constructor (s) { + super(s); + this.m = [1, Math.tan(this.angle.toRadians()), 0, 1, 0, 0]; + } + } + }); + + const data = svg.trim(svg.compressSpaces(v)).replace(/\)([a-zA-Z])/g, ') $1').replace(/\)(\s?,\s?)/g, ') ').split(/\s(?=[a-z])/); + this.transforms = data.map((d) => { + const type = svg.trim(d.split('(')[0]); + const s = d.split('(')[1].replace(')', ''); + const transform = new this.Type[type](s); + transform.type = type; + return transform; + }); + } + + apply (ctx) { + this.transforms.forEach((transform) => { + transform.apply(ctx); + }); + } + + unapply (ctx) { + for (let i = this.transforms.length - 1; i >= 0; i--) { + this.transforms[i].unapply(ctx); + } + } + + applyToPoint (p) { + this.transforms.forEach((transform) => { + transform.applyToPoint(p); + }); + } + }; + + // aspect ratio + svg.AspectRatio = function (ctx, aspectRatio, width, desiredWidth, height, desiredHeight, minX, minY, refX, refY) { + // aspect ratio - https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute + aspectRatio = svg.compressSpaces(aspectRatio); + aspectRatio = aspectRatio.replace(/^defer\s/, ''); // ignore defer + const align = aspectRatio.split(' ')[0] || 'xMidYMid'; + const meetOrSlice = aspectRatio.split(' ')[1] || 'meet'; + + // calculate scale + const scaleX = width / desiredWidth; + const scaleY = height / desiredHeight; + const scaleMin = Math.min(scaleX, scaleY); + const scaleMax = Math.max(scaleX, scaleY); + if (meetOrSlice === 'meet') { desiredWidth *= scaleMin; desiredHeight *= scaleMin; } + if (meetOrSlice === 'slice') { desiredWidth *= scaleMax; desiredHeight *= scaleMax; } + + refX = new svg.Property('refX', refX); + refY = new svg.Property('refY', refY); + if (refX.hasValue() && refY.hasValue()) { + ctx.translate(-scaleMin * refX.toPixels('x'), -scaleMin * refY.toPixels('y')); + } else { + // align + if (align.match(/^xMid/) && ((meetOrSlice === 'meet' && scaleMin === scaleY) || (meetOrSlice === 'slice' && scaleMax === scaleY))) ctx.translate(width / 2.0 - desiredWidth / 2.0, 0); + if (align.match(/YMid$/) && ((meetOrSlice === 'meet' && scaleMin === scaleX) || (meetOrSlice === 'slice' && scaleMax === scaleX))) ctx.translate(0, height / 2.0 - desiredHeight / 2.0); + if (align.match(/^xMax/) && ((meetOrSlice === 'meet' && scaleMin === scaleY) || (meetOrSlice === 'slice' && scaleMax === scaleY))) ctx.translate(width - desiredWidth, 0); + if (align.match(/YMax$/) && ((meetOrSlice === 'meet' && scaleMin === scaleX) || (meetOrSlice === 'slice' && scaleMax === scaleX))) ctx.translate(0, height - desiredHeight); + } + + // scale + if (align === 'none') ctx.scale(scaleX, scaleY); + else if (meetOrSlice === 'meet') ctx.scale(scaleMin, scaleMin); + else if (meetOrSlice === 'slice') ctx.scale(scaleMax, scaleMax); + + // translate + ctx.translate(minX == null ? 0 : -minX, minY == null ? 0 : -minY); + }; + + // elements + svg.Element = {}; + + svg.EmptyProperty = new svg.Property('EMPTY', ''); + + svg.Element.ElementBase = class { + constructor (node) { + this.captureTextNodes = arguments[1]; // Argument from inheriting class + this.attributes = {}; + this.styles = {}; + this.children = []; + if (node != null && node.nodeType === 1) { // ELEMENT_NODE + // add children + [...node.childNodes].forEach((childNode) => { + if (childNode.nodeType === 1) { + this.addChild(childNode, true); // ELEMENT_NODE + } + if (this.captureTextNodes && ( + childNode.nodeType === 3 || childNode.nodeType === 4 + )) { + const text = childNode.nodeValue || childNode.text || ''; + if (svg.trim(svg.compressSpaces(text)) !== '') { + this.addChild(new svg.Element.tspan(childNode), false); // TEXT_NODE + } + } + }); + + // add attributes + [...node.attributes].forEach(({nodeName, nodeValue}) => { + this.attributes[nodeName] = new svg.Property( + nodeName, + nodeValue + ); + }); + // add tag styles + let styles = svg.Styles[node.nodeName]; + if (styles != null) { + for (const name in styles) { + this.styles[name] = styles[name]; + } + } + + // add class styles + if (this.attribute('class').hasValue()) { + const classes = svg.compressSpaces(this.attribute('class').value).split(' '); + classes.forEach((clss) => { + styles = svg.Styles['.' + clss]; + if (styles != null) { + for (const name in styles) { + this.styles[name] = styles[name]; + } + } + styles = svg.Styles[node.nodeName + '.' + clss]; + if (styles != null) { + for (const name in styles) { + this.styles[name] = styles[name]; + } + } + }); + } + + // add id styles + if (this.attribute('id').hasValue()) { + const styles = svg.Styles['#' + this.attribute('id').value]; + if (styles != null) { + for (const name in styles) { + this.styles[name] = styles[name]; + } + } + } + + // add inline styles + if (this.attribute('style').hasValue()) { + const styles = this.attribute('style').value.split(';'); + styles.forEach((style) => { + if (svg.trim(style) !== '') { + let {name, value} = style.split(':'); + name = svg.trim(name); + value = svg.trim(value); + this.styles[name] = new svg.Property(name, value); + } + }); + } + + // add id + if (this.attribute('id').hasValue()) { + if (svg.Definitions[this.attribute('id').value] == null) { + svg.Definitions[this.attribute('id').value] = this; + } + } + } + } + + // get or create attribute + attribute (name, createIfNotExists) { + let a = this.attributes[name]; + if (a != null) return a; + + if (createIfNotExists === true) { a = new svg.Property(name, ''); this.attributes[name] = a; } + return a || svg.EmptyProperty; + } + + getHrefAttribute () { + for (const a in this.attributes) { + if (a.match(/:href$/)) { + return this.attributes[a]; + } + } + return svg.EmptyProperty; + } + + // get or create style, crawls up node tree + style (name, createIfNotExists, skipAncestors) { + let s = this.styles[name]; + if (s != null) return s; + + const a = this.attribute(name); + if (a != null && a.hasValue()) { + this.styles[name] = a; // move up to me to cache + return a; + } + + if (skipAncestors !== true) { + const p = this.parent; + if (p != null) { + const ps = p.style(name); + if (ps != null && ps.hasValue()) { + return ps; + } + } + } + + if (createIfNotExists === true) { s = new svg.Property(name, ''); this.styles[name] = s; } + return s || svg.EmptyProperty; + } + + // base render + render (ctx) { + // don't render display=none + if (this.style('display').value === 'none') return; + + // don't render visibility=hidden + if (this.style('visibility').value === 'hidden') return; + + ctx.save(); + if (this.attribute('mask').hasValue()) { // mask + const mask = this.attribute('mask').getDefinition(); + if (mask != null) mask.apply(ctx, this); + } else if (this.style('filter').hasValue()) { // filter + const filter = this.style('filter').getDefinition(); + if (filter != null) filter.apply(ctx, this); + } else { + this.setContext(ctx); + this.renderChildren(ctx); + this.clearContext(ctx); + } + ctx.restore(); + } + + // base set context + setContext (ctx) { + // OVERRIDE ME! + } + + // base clear context + clearContext (ctx) { + // OVERRIDE ME! + } + + // base render children + renderChildren (ctx) { + this.children.forEach((child) => { + child.render(ctx); + }); + } + + addChild (childNode, create) { + const child = create + ? svg.CreateElement(childNode) + : childNode; + child.parent = this; + if (child.type !== 'title') { this.children.push(child); } + } + }; + + svg.Element.RenderedElementBase = class extends svg.Element.ElementBase { + setContext (ctx) { + // fill + if (this.style('fill').isUrlDefinition()) { + const fs = this.style('fill').getFillStyleDefinition(this, this.style('fill-opacity')); + if (fs != null) ctx.fillStyle = fs; + } else if (this.style('fill').hasValue()) { + const fillStyle = this.style('fill'); + if (fillStyle.value === 'currentColor') fillStyle.value = this.style('color').value; + ctx.fillStyle = (fillStyle.value === 'none' ? 'rgba(0,0,0,0)' : fillStyle.value); + } + if (this.style('fill-opacity').hasValue()) { + let fillStyle = new svg.Property('fill', ctx.fillStyle); + fillStyle = fillStyle.addOpacity(this.style('fill-opacity')); + ctx.fillStyle = fillStyle.value; + } + + // stroke + if (this.style('stroke').isUrlDefinition()) { + const fs = this.style('stroke').getFillStyleDefinition(this, this.style('stroke-opacity')); + if (fs != null) ctx.strokeStyle = fs; + } else if (this.style('stroke').hasValue()) { + const strokeStyle = this.style('stroke'); + if (strokeStyle.value === 'currentColor') strokeStyle.value = this.style('color').value; + ctx.strokeStyle = (strokeStyle.value === 'none' ? 'rgba(0,0,0,0)' : strokeStyle.value); + } + if (this.style('stroke-opacity').hasValue()) { + let strokeStyle = new svg.Property('stroke', ctx.strokeStyle); + strokeStyle = strokeStyle.addOpacity(this.style('stroke-opacity')); + ctx.strokeStyle = strokeStyle.value; + } + if (this.style('stroke-width').hasValue()) { + const newLineWidth = this.style('stroke-width').toPixels(); + ctx.lineWidth = newLineWidth === 0 ? 0.001 : newLineWidth; // browsers don't respect 0 + } + if (this.style('stroke-linecap').hasValue()) ctx.lineCap = this.style('stroke-linecap').value; + if (this.style('stroke-linejoin').hasValue()) ctx.lineJoin = this.style('stroke-linejoin').value; + if (this.style('stroke-miterlimit').hasValue()) ctx.miterLimit = this.style('stroke-miterlimit').value; + if (this.style('stroke-dasharray').hasValue() && this.style('stroke-dasharray').value !== 'none') { + const gaps = svg.ToNumberArray(this.style('stroke-dasharray').value); + if (typeof ctx.setLineDash !== 'undefined') { + ctx.setLineDash(gaps); + } else if (typeof ctx.webkitLineDash !== 'undefined') { + ctx.webkitLineDash = gaps; + } else if (typeof ctx.mozDash !== 'undefined' && !(gaps.length === 1 && gaps[0] === 0)) { + ctx.mozDash = gaps; + } + + const offset = this.style('stroke-dashoffset').numValueOrDefault(1); + if (typeof ctx.lineDashOffset !== 'undefined') { + ctx.lineDashOffset = offset; + } else if (typeof ctx.webkitLineDashOffset !== 'undefined') { + ctx.webkitLineDashOffset = offset; + } else if (typeof ctx.mozDashOffset !== 'undefined') { + ctx.mozDashOffset = offset; + } + } + + // font + if (typeof ctx.font !== 'undefined') { + ctx.font = svg.Font.CreateFont( + this.style('font-style').value, + this.style('font-variant').value, + this.style('font-weight').value, + this.style('font-size').hasValue() ? this.style('font-size').toPixels() + 'px' : '', + this.style('font-family').value).toString(); + } + + // transform + if (this.attribute('transform').hasValue()) { + const transform = new svg.Transform(this.attribute('transform').value); + transform.apply(ctx); + } + + // clip + if (this.style('clip-path', false, true).hasValue()) { + const clip = this.style('clip-path', false, true).getDefinition(); + if (clip != null) clip.apply(ctx); + } + + // opacity + if (this.style('opacity').hasValue()) { + ctx.globalAlpha = this.style('opacity').numValue(); + } + } + }; + + svg.Element.PathElementBase = class extends svg.Element.RenderedElementBase { + path (ctx) { + if (ctx != null) ctx.beginPath(); + return new svg.BoundingBox(); + } + + renderChildren (ctx) { + this.path(ctx); + svg.Mouse.checkPath(this, ctx); + if (ctx.fillStyle !== '') { + if (this.style('fill-rule').valueOrDefault('inherit') !== 'inherit') { + ctx.fill(this.style('fill-rule').value); + } else { + ctx.fill(); + } + } + if (ctx.strokeStyle !== '') ctx.stroke(); + + const markers = this.getMarkers(); + if (markers != null) { + if (this.style('marker-start').isUrlDefinition()) { + const marker = this.style('marker-start').getDefinition(); + marker.render(ctx, markers[0][0], markers[0][1]); + } + if (this.style('marker-mid').isUrlDefinition()) { + const marker = this.style('marker-mid').getDefinition(); + for (let i = 1; i < markers.length - 1; i++) { + marker.render(ctx, markers[i][0], markers[i][1]); + } + } + if (this.style('marker-end').isUrlDefinition()) { + const marker = this.style('marker-end').getDefinition(); + marker.render(ctx, markers[markers.length - 1][0], markers[markers.length - 1][1]); + } + } + } + + getBoundingBox () { + return this.path(); + } + + getMarkers () { + return null; + } + }; + + // svg element + svg.Element.svg = class extends svg.Element.RenderedElementBase { + clearContext (ctx) { + super.clearContext(ctx); + svg.ViewPort.RemoveCurrent(); + } + + setContext (ctx) { + // initial values and defaults + ctx.strokeStyle = 'rgba(0,0,0,0)'; + ctx.lineCap = 'butt'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 4; + if (typeof ctx.font !== 'undefined' && typeof window.getComputedStyle !== 'undefined') { + ctx.font = window.getComputedStyle(ctx.canvas).getPropertyValue('font'); + } + + super.setContext(ctx); + + // create new view port + if (!this.attribute('x').hasValue()) this.attribute('x', true).value = 0; + if (!this.attribute('y').hasValue()) this.attribute('y', true).value = 0; + ctx.translate(this.attribute('x').toPixels('x'), this.attribute('y').toPixels('y')); + + let width = svg.ViewPort.width(); + let height = svg.ViewPort.height(); + + if (!this.attribute('width').hasValue()) this.attribute('width', true).value = '100%'; + if (!this.attribute('height').hasValue()) this.attribute('height', true).value = '100%'; + if (typeof this.root === 'undefined') { + width = this.attribute('width').toPixels('x'); + height = this.attribute('height').toPixels('y'); + + let x = 0; + let y = 0; + if (this.attribute('refX').hasValue() && this.attribute('refY').hasValue()) { + x = -this.attribute('refX').toPixels('x'); + y = -this.attribute('refY').toPixels('y'); + } + + if (this.attribute('overflow').valueOrDefault('hidden') !== 'visible') { + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(width, y); + ctx.lineTo(width, height); + ctx.lineTo(x, height); + ctx.closePath(); + ctx.clip(); + } + } + svg.ViewPort.SetCurrent(width, height); + + // viewbox + if (this.attribute('viewBox').hasValue()) { + const viewBox = svg.ToNumberArray(this.attribute('viewBox').value); + const minX = viewBox[0]; + const minY = viewBox[1]; + width = viewBox[2]; + height = viewBox[3]; + + svg.AspectRatio( + ctx, + this.attribute('preserveAspectRatio').value, + svg.ViewPort.width(), + width, + svg.ViewPort.height(), + height, + minX, + minY, + this.attribute('refX').value, + this.attribute('refY').value + ); + + svg.ViewPort.RemoveCurrent(); + svg.ViewPort.SetCurrent(viewBox[2], viewBox[3]); + } + } + }; + + // rect element + svg.Element.rect = class extends svg.Element.PathElementBase { + path (ctx) { + const x = this.attribute('x').toPixels('x'); + const y = this.attribute('y').toPixels('y'); + const width = this.attribute('width').toPixels('x'); + const height = this.attribute('height').toPixels('y'); + let rx = this.attribute('rx').toPixels('x'); + let ry = this.attribute('ry').toPixels('y'); + if (this.attribute('rx').hasValue() && !this.attribute('ry').hasValue()) ry = rx; + if (this.attribute('ry').hasValue() && !this.attribute('rx').hasValue()) rx = ry; + rx = Math.min(rx, width / 2.0); + ry = Math.min(ry, height / 2.0); + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(x + rx, y); + ctx.lineTo(x + width - rx, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + ry); + ctx.lineTo(x + width, y + height - ry); + ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height); + ctx.lineTo(x + rx, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - ry); + ctx.lineTo(x, y + ry); + ctx.quadraticCurveTo(x, y, x + rx, y); + ctx.closePath(); + } + + return new svg.BoundingBox(x, y, x + width, y + height); + } + }; + + // circle element + svg.Element.circle = class extends svg.Element.PathElementBase { + path (ctx) { + const cx = this.attribute('cx').toPixels('x'); + const cy = this.attribute('cy').toPixels('y'); + const r = this.attribute('r').toPixels(); + + if (ctx != null) { + ctx.beginPath(); + ctx.arc(cx, cy, r, 0, Math.PI * 2, true); + ctx.closePath(); + } + + return new svg.BoundingBox(cx - r, cy - r, cx + r, cy + r); + } + }; + + // ellipse element + const KAPPA = 4 * ((Math.sqrt(2) - 1) / 3); + svg.Element.ellipse = class extends svg.Element.PathElementBase { + path (ctx) { + const rx = this.attribute('rx').toPixels('x'); + const ry = this.attribute('ry').toPixels('y'); + const cx = this.attribute('cx').toPixels('x'); + const cy = this.attribute('cy').toPixels('y'); + + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(cx, cy - ry); + ctx.bezierCurveTo(cx + (KAPPA * rx), cy - ry, cx + rx, cy - (KAPPA * ry), cx + rx, cy); + ctx.bezierCurveTo(cx + rx, cy + (KAPPA * ry), cx + (KAPPA * rx), cy + ry, cx, cy + ry); + ctx.bezierCurveTo(cx - (KAPPA * rx), cy + ry, cx - rx, cy + (KAPPA * ry), cx - rx, cy); + ctx.bezierCurveTo(cx - rx, cy - (KAPPA * ry), cx - (KAPPA * rx), cy - ry, cx, cy - ry); + ctx.closePath(); + } + + return new svg.BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry); + } + }; + + // line element + svg.Element.line = class extends svg.Element.PathElementBase { + getPoints () { + return [ + new svg.Point(this.attribute('x1').toPixels('x'), this.attribute('y1').toPixels('y')), + new svg.Point(this.attribute('x2').toPixels('x'), this.attribute('y2').toPixels('y'))]; + } + + path (ctx) { + const points = this.getPoints(); + + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + ctx.lineTo(points[1].x, points[1].y); + } + + return new svg.BoundingBox(points[0].x, points[0].y, points[1].x, points[1].y); + } + + getMarkers () { + const points = this.getPoints(); + const a = points[0].angleTo(points[1]); + return [[points[0], a], [points[1], a]]; + } + }; + + // polyline element + svg.Element.polyline = class extends svg.Element.PathElementBase { + constructor (node) { + super(node); + + this.points = svg.CreatePath(this.attribute('points').value); + } + path (ctx) { + const {x, y} = this.points[0]; + const bb = new svg.BoundingBox(x, y); + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(x, y); + } + for (let i = 1; i < this.points.length; i++) { + const {x, y} = this.points[i]; + bb.addPoint(x, y); + if (ctx != null) ctx.lineTo(x, y); + } + return bb; + } + + getMarkers () { + const markers = []; + for (let i = 0; i < this.points.length - 1; i++) { + markers.push([this.points[i], this.points[i].angleTo(this.points[i + 1])]); + } + markers.push([this.points[this.points.length - 1], markers[markers.length - 1][1]]); + return markers; + } + }; + + // polygon element + svg.Element.polygon = class extends svg.Element.polyline { + path (ctx) { + const bb = super.path(ctx); + if (ctx != null) { + ctx.lineTo(this.points[0].x, this.points[0].y); + ctx.closePath(); + } + return bb; + } + }; + + // path element + svg.Element.path = class extends svg.Element.PathElementBase { + constructor (node) { + super(node); + + let d = this.attribute('d').value + // TODO: convert to real lexer based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF + .replace(/,/gm, ' ') // get rid of all commas + .replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from commands + .replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from commands + .replace(/([MmZzLlHhVvCcSsQqTtAa])([^\s])/gm, '$1 $2') // separate commands from points + .replace(/([^\s])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from points + .replace(/([0-9])([+-])/gm, '$1 $2') // separate digits when no comma + .replace(/(\.[0-9]*)(\.)/gm, '$1 $2') // separate digits when no comma + .replace(/([Aa](\s+[0-9]+){3})\s+([01])\s*([01])/gm, '$1 $3 $4 '); // shorthand elliptical arc path syntax + d = svg.compressSpaces(d); // compress multiple spaces + d = svg.trim(d); + this.PathParser = { + tokens: d.split(' '), + + reset () { + this.i = -1; + this.command = ''; + this.previousCommand = ''; + this.start = new svg.Point(0, 0); + this.control = new svg.Point(0, 0); + this.current = new svg.Point(0, 0); + this.points = []; + this.angles = []; + }, + + isEnd () { + return this.i >= this.tokens.length - 1; + }, + + isCommandOrEnd () { + if (this.isEnd()) return true; + return this.tokens[this.i + 1].match(/^[A-Za-z]$/) != null; + }, + + isRelativeCommand () { + switch (this.command) { + case 'm': + case 'l': + case 'h': + case 'v': + case 'c': + case 's': + case 'q': + case 't': + case 'a': + case 'z': + return true; + } + return false; + }, + + getToken () { + this.i++; + return this.tokens[this.i]; + }, + + getScalar () { + return parseFloat(this.getToken()); + }, + + nextCommand () { + this.previousCommand = this.command; + this.command = this.getToken(); + }, + + getPoint () { + const p = new svg.Point(this.getScalar(), this.getScalar()); + return this.makeAbsolute(p); + }, + + getAsControlPoint () { + const p = this.getPoint(); + this.control = p; + return p; + }, + + getAsCurrentPoint () { + const p = this.getPoint(); + this.current = p; + return p; + }, + + getReflectedControlPoint () { + if (this.previousCommand.toLowerCase() !== 'c' && + this.previousCommand.toLowerCase() !== 's' && + this.previousCommand.toLowerCase() !== 'q' && + this.previousCommand.toLowerCase() !== 't') { + return this.current; + } + + // reflect point + const p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y); + return p; + }, + + makeAbsolute (p) { + if (this.isRelativeCommand()) { + p.x += this.current.x; + p.y += this.current.y; + } + return p; + }, + + addMarker (p, from, priorTo) { + // if the last angle isn't filled in because we didn't have this point yet ... + if (priorTo != null && this.angles.length > 0 && this.angles[this.angles.length - 1] == null) { + this.angles[this.angles.length - 1] = this.points[this.points.length - 1].angleTo(priorTo); + } + this.addMarkerAngle(p, from == null ? null : from.angleTo(p)); + }, + + addMarkerAngle (p, a) { + this.points.push(p); + this.angles.push(a); + }, + + getMarkerPoints () { return this.points; }, + getMarkerAngles () { + for (let i = 0; i < this.angles.length; i++) { + if (this.angles[i] == null) { + for (let j = i + 1; j < this.angles.length; j++) { + if (this.angles[j] != null) { + this.angles[i] = this.angles[j]; + break; + } + } + } + } + return this.angles; + } + }; + } + + path (ctx) { + const pp = this.PathParser; + pp.reset(); + + const bb = new svg.BoundingBox(); + if (ctx != null) ctx.beginPath(); + while (!pp.isEnd()) { + pp.nextCommand(); + switch (pp.command) { + case 'M': + case 'm': + const p = pp.getAsCurrentPoint(); + pp.addMarker(p); + bb.addPoint(p.x, p.y); + if (ctx != null) ctx.moveTo(p.x, p.y); + pp.start = pp.current; + while (!pp.isCommandOrEnd()) { + const p = pp.getAsCurrentPoint(); + pp.addMarker(p, pp.start); + bb.addPoint(p.x, p.y); + if (ctx != null) ctx.lineTo(p.x, p.y); + } + break; + case 'L': + case 'l': + while (!pp.isCommandOrEnd()) { + const c = pp.current; + const p = pp.getAsCurrentPoint(); + pp.addMarker(p, c); + bb.addPoint(p.x, p.y); + if (ctx != null) ctx.lineTo(p.x, p.y); + } + break; + case 'H': + case 'h': + while (!pp.isCommandOrEnd()) { + const newP = new svg.Point((pp.isRelativeCommand() ? pp.current.x : 0) + pp.getScalar(), pp.current.y); + pp.addMarker(newP, pp.current); + pp.current = newP; + bb.addPoint(pp.current.x, pp.current.y); + if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y); + } + break; + case 'V': + case 'v': + while (!pp.isCommandOrEnd()) { + const newP = new svg.Point(pp.current.x, (pp.isRelativeCommand() ? pp.current.y : 0) + pp.getScalar()); + pp.addMarker(newP, pp.current); + pp.current = newP; + bb.addPoint(pp.current.x, pp.current.y); + if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y); + } + break; + case 'C': + case 'c': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + const p1 = pp.getPoint(); + const cntrl = pp.getAsControlPoint(); + const cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, p1); + bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'S': + case 's': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + const p1 = pp.getReflectedControlPoint(); + const cntrl = pp.getAsControlPoint(); + const cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, p1); + bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'Q': + case 'q': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + const cntrl = pp.getAsControlPoint(); + const cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, cntrl); + bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'T': + case 't': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + const cntrl = pp.getReflectedControlPoint(); + pp.control = cntrl; + const cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, cntrl); + bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'A': + case 'a': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + let rx = pp.getScalar(); + let ry = pp.getScalar(); + const xAxisRotation = pp.getScalar() * (Math.PI / 180.0); + const largeArcFlag = pp.getScalar(); + const sweepFlag = pp.getScalar(); + const cp = pp.getAsCurrentPoint(); + + // Conversion from endpoint to center parameterization + // https://www.w3.org/TR/SVG11/implnote.html#ArcConversionEndpointToCenter + + // x1', y1' + const currp = new svg.Point( + Math.cos(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.sin(xAxisRotation) * (curr.y - cp.y) / 2.0, + -Math.sin(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.cos(xAxisRotation) * (curr.y - cp.y) / 2.0 + ); + // adjust radii + const l = Math.pow(currp.x, 2) / Math.pow(rx, 2) + Math.pow(currp.y, 2) / Math.pow(ry, 2); + if (l > 1) { + rx *= Math.sqrt(l); + ry *= Math.sqrt(l); + } + // cx', cy' + let s = (largeArcFlag === sweepFlag ? -1 : 1) * Math.sqrt( + ((Math.pow(rx, 2) * Math.pow(ry, 2)) - (Math.pow(rx, 2) * Math.pow(currp.y, 2)) - (Math.pow(ry, 2) * Math.pow(currp.x, 2))) / + (Math.pow(rx, 2) * Math.pow(currp.y, 2) + Math.pow(ry, 2) * Math.pow(currp.x, 2)) + ); + if (isNaN(s)) s = 0; + const cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); + // cx, cy + const centp = new svg.Point( + (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, + (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y + ); + // vector magnitude + const m = function (v) { + return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2)); + }; + // ratio between two vectors + const r = function (u, v) { + return (u[0] * v[0] + u[1] * v[1]) / (m(u) * m(v)); + }; + // angle between two vectors + const a = function (u, v) { + return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(r(u, v)); + }; + // initial angle + const a1 = a([1, 0], [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]); + // angle delta + const u = [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]; + const v = [(-currp.x - cpp.x) / rx, (-currp.y - cpp.y) / ry]; + let ad = a(u, v); + if (r(u, v) <= -1) ad = Math.PI; + if (r(u, v) >= 1) ad = 0; + + // for markers + const dir = 1 - sweepFlag ? 1.0 : -1.0; + const ah = a1 + dir * (ad / 2.0); + const halfWay = new svg.Point( + centp.x + rx * Math.cos(ah), + centp.y + ry * Math.sin(ah) + ); + pp.addMarkerAngle(halfWay, ah - dir * Math.PI / 2); + pp.addMarkerAngle(cp, ah - dir * Math.PI); + + bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better + if (ctx != null) { + const r = rx > ry ? rx : ry; + const sx = rx > ry ? 1 : rx / ry; + const sy = rx > ry ? ry / rx : 1; + + ctx.translate(centp.x, centp.y); + ctx.rotate(xAxisRotation); + ctx.scale(sx, sy); + ctx.arc(0, 0, r, a1, a1 + ad, 1 - sweepFlag); + ctx.scale(1 / sx, 1 / sy); + ctx.rotate(-xAxisRotation); + ctx.translate(-centp.x, -centp.y); + } + } + break; + case 'Z': + case 'z': + if (ctx != null) ctx.closePath(); + pp.current = pp.start; + } + } + + return bb; + } + + getMarkers () { + const points = this.PathParser.getMarkerPoints(); + const angles = this.PathParser.getMarkerAngles(); + + const markers = points.map((point, i) => { + return [point, angles[i]]; + }); + return markers; + } + }; + + // pattern element + svg.Element.pattern = class extends svg.Element.ElementBase { + createPattern (ctx, element) { + const width = this.attribute('width').toPixels('x', true); + const height = this.attribute('height').toPixels('y', true); + + // render me using a temporary svg element + const tempSvg = new svg.Element.svg(); + tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value); + tempSvg.attributes['width'] = new svg.Property('width', width + 'px'); + tempSvg.attributes['height'] = new svg.Property('height', height + 'px'); + tempSvg.attributes['transform'] = new svg.Property('transform', this.attribute('patternTransform').value); + tempSvg.children = this.children; + + const c = document.createElement('canvas'); + c.width = width; + c.height = height; + const cctx = c.getContext('2d'); + if (this.attribute('x').hasValue() && this.attribute('y').hasValue()) { + cctx.translate(this.attribute('x').toPixels('x', true), this.attribute('y').toPixels('y', true)); + } + // render 3x3 grid so when we transform there's no white space on edges + for (let x = -1; x <= 1; x++) { + for (let y = -1; y <= 1; y++) { + cctx.save(); + cctx.translate(x * c.width, y * c.height); + tempSvg.render(cctx); + cctx.restore(); + } + } + const pattern = ctx.createPattern(c, 'repeat'); + return pattern; + } + }; + + // marker element + svg.Element.marker = class extends svg.Element.ElementBase { + render (ctx, point, angle) { + ctx.translate(point.x, point.y); + if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(angle); + if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(ctx.lineWidth, ctx.lineWidth); + ctx.save(); + + // render me using a temporary svg element + const tempSvg = new svg.Element.svg(); + tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value); + tempSvg.attributes['refX'] = new svg.Property('refX', this.attribute('refX').value); + tempSvg.attributes['refY'] = new svg.Property('refY', this.attribute('refY').value); + tempSvg.attributes['width'] = new svg.Property('width', this.attribute('markerWidth').value); + tempSvg.attributes['height'] = new svg.Property('height', this.attribute('markerHeight').value); + tempSvg.attributes['fill'] = new svg.Property('fill', this.attribute('fill').valueOrDefault('black')); + tempSvg.attributes['stroke'] = new svg.Property('stroke', this.attribute('stroke').valueOrDefault('none')); + tempSvg.children = this.children; + tempSvg.render(ctx); + + ctx.restore(); + if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(1 / ctx.lineWidth, 1 / ctx.lineWidth); + if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(-angle); + ctx.translate(-point.x, -point.y); + } + }; + + // definitions element + svg.Element.defs = class extends svg.Element.ElementBase { + render (ctx) { + // NOOP + } + }; + + // base for gradients + svg.Element.GradientBase = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + this.gradientUnits = this.attribute('gradientUnits').valueOrDefault('objectBoundingBox'); + + this.stops = []; + this.children.forEach((child) => { + if (child.type === 'stop') { + this.stops.push(child); + } + }); + } + + getGradient () { + // OVERRIDE ME! + } + + createGradient (ctx, element, parentOpacityProp) { + const stopsContainer = this.getHrefAttribute().hasValue() + ? this.getHrefAttribute().getDefinition() + : this; + + const addParentOpacity = function (color) { + if (parentOpacityProp.hasValue()) { + const p = new svg.Property('color', color); + return p.addOpacity(parentOpacityProp).value; + } + return color; + }; + + const g = this.getGradient(ctx, element); + if (g == null) return addParentOpacity(stopsContainer.stops[stopsContainer.stops.length - 1].color); + stopsContainer.stops.forEach(({offset, color}) => { + g.addColorStop(offset, addParentOpacity(color)); + }); + + if (this.attribute('gradientTransform').hasValue()) { + // render as transformed pattern on temporary canvas + const rootView = svg.ViewPort.viewPorts[0]; + + const rect = new svg.Element.rect(); + rect.attributes['x'] = new svg.Property('x', -svg.MAX_VIRTUAL_PIXELS / 3.0); + rect.attributes['y'] = new svg.Property('y', -svg.MAX_VIRTUAL_PIXELS / 3.0); + rect.attributes['width'] = new svg.Property('width', svg.MAX_VIRTUAL_PIXELS); + rect.attributes['height'] = new svg.Property('height', svg.MAX_VIRTUAL_PIXELS); + + const group = new svg.Element.g(); + group.attributes['transform'] = new svg.Property('transform', this.attribute('gradientTransform').value); + group.children = [rect]; + + const tempSvg = new svg.Element.svg(); + tempSvg.attributes['x'] = new svg.Property('x', 0); + tempSvg.attributes['y'] = new svg.Property('y', 0); + tempSvg.attributes['width'] = new svg.Property('width', rootView.width); + tempSvg.attributes['height'] = new svg.Property('height', rootView.height); + tempSvg.children = [group]; + + const c = document.createElement('canvas'); + c.width = rootView.width; + c.height = rootView.height; + const tempCtx = c.getContext('2d'); + tempCtx.fillStyle = g; + tempSvg.render(tempCtx); + return tempCtx.createPattern(c, 'no-repeat'); + } + + return g; + } + }; + + // linear gradient element + svg.Element.linearGradient = class extends svg.Element.GradientBase { + getGradient (ctx, element) { + const useBB = this.gradientUnits === 'objectBoundingBox' && element.getBoundingBox; + const bb = useBB + ? element.getBoundingBox() + : null; + + if (!this.attribute('x1').hasValue() && + !this.attribute('y1').hasValue() && + !this.attribute('x2').hasValue() && + !this.attribute('y2').hasValue() + ) { + this.attribute('x1', true).value = 0; + this.attribute('y1', true).value = 0; + this.attribute('x2', true).value = 1; + this.attribute('y2', true).value = 0; + } + + const x1 = (useBB + ? bb.x() + bb.width() * this.attribute('x1').numValue() + : this.attribute('x1').toPixels('x')); + const y1 = (useBB + ? bb.y() + bb.height() * this.attribute('y1').numValue() + : this.attribute('y1').toPixels('y')); + const x2 = (useBB + ? bb.x() + bb.width() * this.attribute('x2').numValue() + : this.attribute('x2').toPixels('x')); + const y2 = (useBB + ? bb.y() + bb.height() * this.attribute('y2').numValue() + : this.attribute('y2').toPixels('y')); + + if (x1 === x2 && y1 === y2) return null; + return ctx.createLinearGradient(x1, y1, x2, y2); + } + }; + + // radial gradient element + svg.Element.radialGradient = class extends svg.Element.GradientBase { + getGradient (ctx, element) { + const useBB = this.gradientUnits === 'objectBoundingBox' && element.getBoundingBox; + const bb = useBB ? element.getBoundingBox() : null; + + if (!this.attribute('cx').hasValue()) this.attribute('cx', true).value = '50%'; + if (!this.attribute('cy').hasValue()) this.attribute('cy', true).value = '50%'; + if (!this.attribute('r').hasValue()) this.attribute('r', true).value = '50%'; + + const cx = (useBB + ? bb.x() + bb.width() * this.attribute('cx').numValue() + : this.attribute('cx').toPixels('x')); + const cy = (useBB + ? bb.y() + bb.height() * this.attribute('cy').numValue() + : this.attribute('cy').toPixels('y')); + + let fx = cx; + let fy = cy; + if (this.attribute('fx').hasValue()) { + fx = (useBB + ? bb.x() + bb.width() * this.attribute('fx').numValue() + : this.attribute('fx').toPixels('x')); + } + if (this.attribute('fy').hasValue()) { + fy = (useBB + ? bb.y() + bb.height() * this.attribute('fy').numValue() + : this.attribute('fy').toPixels('y')); + } + + const r = (useBB + ? (bb.width() + bb.height()) / 2.0 * this.attribute('r').numValue() + : this.attribute('r').toPixels()); + + return ctx.createRadialGradient(fx, fy, 0, cx, cy, r); + } + }; + + // gradient stop element + svg.Element.stop = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + this.offset = this.attribute('offset').numValue(); + if (this.offset < 0) this.offset = 0; + if (this.offset > 1) this.offset = 1; + + let stopColor = this.style('stop-color'); + if (this.style('stop-opacity').hasValue()) { + stopColor = stopColor.addOpacity(this.style('stop-opacity')); + } + this.color = stopColor.value; + } + }; + + // animation base element + svg.Element.AnimateBase = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + svg.Animations.push(this); + + this.duration = 0.0; + this.begin = this.attribute('begin').toMilliseconds(); + this.maxDuration = this.begin + this.attribute('dur').toMilliseconds(); + + this.initialValue = null; + this.initialUnits = ''; + this.removed = false; + + this.from = this.attribute('from'); + this.to = this.attribute('to'); + this.values = this.attribute('values'); + if (this.values.hasValue()) this.values.value = this.values.value.split(';'); + } + + getProperty () { + const attributeType = this.attribute('attributeType').value; + const attributeName = this.attribute('attributeName').value; + + if (attributeType === 'CSS') { + return this.parent.style(attributeName, true); + } + return this.parent.attribute(attributeName, true); + } + + calcValue () { + // OVERRIDE ME! + return ''; + } + + update (delta) { + // set initial value + if (this.initialValue == null) { + this.initialValue = this.getProperty().value; + this.initialUnits = this.getProperty().getUnits(); + } + + // if we're past the end time + if (this.duration > this.maxDuration) { + // loop for indefinitely repeating animations + if (this.attribute('repeatCount').value === 'indefinite' || + this.attribute('repeatDur').value === 'indefinite') { + this.duration = 0.0; + } else if (this.attribute('fill').valueOrDefault('remove') === 'freeze' && !this.frozen) { + this.frozen = true; + this.parent.animationFrozen = true; + this.parent.animationFrozenValue = this.getProperty().value; + } else if (this.attribute('fill').valueOrDefault('remove') === 'remove' && !this.removed) { + this.removed = true; + this.getProperty().value = this.parent.animationFrozen ? this.parent.animationFrozenValue : this.initialValue; + return true; + } + return false; + } + this.duration = this.duration + delta; + + // if we're past the begin time + let updated = false; + if (this.begin < this.duration) { + let newValue = this.calcValue(); // tween + + if (this.attribute('type').hasValue()) { + // for transform, etc. + const type = this.attribute('type').value; + newValue = type + '(' + newValue + ')'; + } + + this.getProperty().value = newValue; + updated = true; + } + + return updated; + } + + // fraction of duration we've covered + progress () { + const ret = { progress: (this.duration - this.begin) / (this.maxDuration - this.begin) }; + if (this.values.hasValue()) { + const p = ret.progress * (this.values.value.length - 1); + const lb = Math.floor(p), ub = Math.ceil(p); + ret.from = new svg.Property('from', parseFloat(this.values.value[lb])); + ret.to = new svg.Property('to', parseFloat(this.values.value[ub])); + ret.progress = (p - lb) / (ub - lb); + } else { + ret.from = this.from; + ret.to = this.to; + } + return ret; + } + }; + + // animate element + svg.Element.animate = class extends svg.Element.AnimateBase { + calcValue () { + const p = this.progress(); + + // tween value linearly + const newValue = p.from.numValue() + (p.to.numValue() - p.from.numValue()) * p.progress; + return newValue + this.initialUnits; + } + }; + + // animate color element + svg.Element.animateColor = class extends svg.Element.AnimateBase { + calcValue () { + const p = this.progress(); + const from = new RGBColor(p.from.value); + const to = new RGBColor(p.to.value); + + if (from.ok && to.ok) { + // tween color linearly + const r = from.r + (to.r - from.r) * p.progress; + const g = from.g + (to.g - from.g) * p.progress; + const b = from.b + (to.b - from.b) * p.progress; + return 'rgb(' + parseInt(r, 10) + ',' + parseInt(g, 10) + ',' + parseInt(b, 10) + ')'; + } + return this.attribute('from').value; + } + }; + + // animate transform element + svg.Element.animateTransform = class extends svg.Element.animate { + calcValue () { + const p = this.progress(); + + // tween value linearly + const from = svg.ToNumberArray(p.from.value); + const to = svg.ToNumberArray(p.to.value); + let newValue = ''; + from.forEach((fr, i) => { + newValue += fr + (to[i] - fr) * p.progress + ' '; + }); + return newValue; + } + }; + + // font element + svg.Element.font = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + this.horizAdvX = this.attribute('horiz-adv-x').numValue(); + + this.isRTL = false; + this.isArabic = false; + this.fontFace = null; + this.missingGlyph = null; + this.glyphs = []; + this.children.forEach((child) => { + if (child.type === 'font-face') { + this.fontFace = child; + if (child.style('font-family').hasValue()) { + svg.Definitions[child.style('font-family').value] = this; + } + } else if (child.type === 'missing-glyph') { + this.missingGlyph = child; + } else if (child.type === 'glyph') { + if (child.arabicForm !== '') { + this.isRTL = true; + this.isArabic = true; + if (typeof this.glyphs[child.unicode] === 'undefined') { + this.glyphs[child.unicode] = []; + } + this.glyphs[child.unicode][child.arabicForm] = child; + } else { + this.glyphs[child.unicode] = child; + } + } + }); + } + }; + + // font-face element + svg.Element.fontface = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + this.ascent = this.attribute('ascent').value; + this.descent = this.attribute('descent').value; + this.unitsPerEm = this.attribute('units-per-em').numValue(); + } + }; + + // missing-glyph element + svg.Element.missingglyph = class extends svg.Element.path { + constructor (node) { + super(node); + + this.horizAdvX = 0; + } + }; + + // glyph element + svg.Element.glyph = class extends svg.Element.path { + constructor (node) { + super(node); + + this.horizAdvX = this.attribute('horiz-adv-x').numValue(); + this.unicode = this.attribute('unicode').value; + this.arabicForm = this.attribute('arabic-form').value; + } + }; + + // text element + svg.Element.text = class extends svg.Element.RenderedElementBase { + constructor (node) { + super(node, true); + } + + setContext (ctx) { + super.setContext(ctx); + + let textBaseline = this.style('dominant-baseline').toTextBaseline(); + if (textBaseline == null) textBaseline = this.style('alignment-baseline').toTextBaseline(); + if (textBaseline != null) ctx.textBaseline = textBaseline; + } + + getBoundingBox () { + const x = this.attribute('x').toPixels('x'); + const y = this.attribute('y').toPixels('y'); + const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + return new svg.BoundingBox(x, y - fontSize, x + Math.floor(fontSize * 2.0 / 3.0) * this.children[0].getText().length, y); + } + + renderChildren (ctx) { + this.x = this.attribute('x').toPixels('x'); + this.y = this.attribute('y').toPixels('y'); + this.x += this.getAnchorDelta(ctx, this, 0); + this.children.forEach((child, i) => { + this.renderChild(ctx, this, i); + }); + } + + getAnchorDelta (ctx, parent, startI) { + const textAnchor = this.style('text-anchor').valueOrDefault('start'); + if (textAnchor !== 'start') { + let width = 0; + for (let i = startI; i < parent.children.length; i++) { + const child = parent.children[i]; + if (i > startI && child.attribute('x').hasValue()) break; // new group + width += child.measureTextRecursive(ctx); + } + return -1 * (textAnchor === 'end' ? width : width / 2.0); + } + return 0; + } + + renderChild (ctx, parent, i) { + const child = parent.children[i]; + if (child.attribute('x').hasValue()) { + child.x = child.attribute('x').toPixels('x') + this.getAnchorDelta(ctx, parent, i); + if (child.attribute('dx').hasValue()) child.x += child.attribute('dx').toPixels('x'); + } else { + if (this.attribute('dx').hasValue()) this.x += this.attribute('dx').toPixels('x'); + if (child.attribute('dx').hasValue()) this.x += child.attribute('dx').toPixels('x'); + child.x = this.x; + } + this.x = child.x + child.measureText(ctx); + + if (child.attribute('y').hasValue()) { + child.y = child.attribute('y').toPixels('y'); + if (child.attribute('dy').hasValue()) child.y += child.attribute('dy').toPixels('y'); + } else { + if (this.attribute('dy').hasValue()) this.y += this.attribute('dy').toPixels('y'); + if (child.attribute('dy').hasValue()) this.y += child.attribute('dy').toPixels('y'); + child.y = this.y; + } + this.y = child.y; + + child.render(ctx); + + for (let i = 0; i < child.children.length; i++) { + this.renderChild(ctx, child, i); + } + } + }; + + // text base + svg.Element.TextElementBase = class extends svg.Element.RenderedElementBase { + getGlyph (font, text, i) { + const c = text[i]; + let glyph = null; + if (font.isArabic) { + let arabicForm = 'isolated'; + if ((i === 0 || text[i - 1] === ' ') && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'terminal'; + if (i > 0 && text[i - 1] !== ' ' && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'medial'; + if (i > 0 && text[i - 1] !== ' ' && (i === text.length - 1 || text[i + 1] === ' ')) arabicForm = 'initial'; + if (typeof font.glyphs[c] !== 'undefined') { + glyph = font.glyphs[c][arabicForm]; + if (glyph == null && font.glyphs[c].type === 'glyph') glyph = font.glyphs[c]; + } + } else { + glyph = font.glyphs[c]; + } + if (glyph == null) glyph = font.missingGlyph; + return glyph; + } + + renderChildren (ctx) { + const customFont = this.parent.style('font-family').getDefinition(); + if (customFont != null) { + const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + const fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle); + let text = this.getText(); + if (customFont.isRTL) text = text.split('').reverse().join(''); + + const dx = svg.ToNumberArray(this.parent.attribute('dx').value); + for (let i = 0; i < text.length; i++) { + const glyph = this.getGlyph(customFont, text, i); + const scale = fontSize / customFont.fontFace.unitsPerEm; + ctx.translate(this.x, this.y); + ctx.scale(scale, -scale); + const lw = ctx.lineWidth; + ctx.lineWidth = ctx.lineWidth * customFont.fontFace.unitsPerEm / fontSize; + if (fontStyle === 'italic') ctx.transform(1, 0, 0.4, 1, 0, 0); + glyph.render(ctx); + if (fontStyle === 'italic') ctx.transform(1, 0, -0.4, 1, 0, 0); + ctx.lineWidth = lw; + ctx.scale(1 / scale, -1 / scale); + ctx.translate(-this.x, -this.y); + + this.x += fontSize * (glyph.horizAdvX || customFont.horizAdvX) / customFont.fontFace.unitsPerEm; + if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { + this.x += dx[i]; + } + } + return; + } + + if (ctx.fillStyle !== '') ctx.fillText(svg.compressSpaces(this.getText()), this.x, this.y); + if (ctx.strokeStyle !== '') ctx.strokeText(svg.compressSpaces(this.getText()), this.x, this.y); + } + + getText () { + // OVERRIDE ME + } + + measureTextRecursive (ctx) { + let width = this.measureText(ctx); + this.children.forEach((child) => { + width += child.measureTextRecursive(ctx); + }); + return width; + } + + measureText (ctx) { + const customFont = this.parent.style('font-family').getDefinition(); + if (customFont != null) { + const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + let measure = 0; + let text = this.getText(); + if (customFont.isRTL) text = text.split('').reverse().join(''); + const dx = svg.ToNumberArray(this.parent.attribute('dx').value); + for (let i = 0; i < text.length; i++) { + const glyph = this.getGlyph(customFont, text, i); + measure += (glyph.horizAdvX || customFont.horizAdvX) * fontSize / customFont.fontFace.unitsPerEm; + if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { + measure += dx[i]; + } + } + return measure; + } + + const textToMeasure = svg.compressSpaces(this.getText()); + if (!ctx.measureText) return textToMeasure.length * 10; + + ctx.save(); + this.setContext(ctx); + const {width} = ctx.measureText(textToMeasure); + ctx.restore(); + return width; + } + }; + + // tspan + svg.Element.tspan = class extends svg.Element.TextElementBase { + constructor (node) { + super(node, true); + + this.text = node.nodeValue || node.text || ''; + } + getText () { + return this.text; + } + }; + + // tref + svg.Element.tref = class extends svg.Element.TextElementBase { + getText () { + const element = this.getHrefAttribute().getDefinition(); + if (element != null) return element.children[0].getText(); + } + }; + + // a element + svg.Element.a = class extends svg.Element.TextElementBase { + constructor (node) { + super(node); + + this.hasText = true; + [...node.childNodes].forEach((childNode) => { + if (childNode.nodeType !== 3) { + this.hasText = false; + } + }); + // this might contain text + this.text = this.hasText ? node.childNodes[0].nodeValue : ''; + } + + getText () { + return this.text; + } + + renderChildren (ctx) { + if (this.hasText) { + // render as text element + super.renderChildren(ctx); + const fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + svg.Mouse.checkBoundingBox(this, new svg.BoundingBox(this.x, this.y - fontSize.toPixels('y'), this.x + this.measureText(ctx), this.y)); + } else { + // render as temporary group + const g = new svg.Element.g(); + g.children = this.children; + g.parent = this; + g.render(ctx); + } + } + + onclick () { + window.open(this.getHrefAttribute().value); + } + + onmousemove () { + svg.ctx.canvas.style.cursor = 'pointer'; + } + }; + + // image element + svg.Element.image = class extends svg.Element.RenderedElementBase { + constructor (node) { + super(node); + + const href = this.getHrefAttribute().value; + if (href === '') { + return; + } + this._isSvg = href.match(/\.svg$/); + + svg.Images.push(this); + this.loaded = false; + if (!this._isSvg) { + this.img = document.createElement('img'); + if (svg.opts.useCORS === true) { + this.img.crossOrigin = 'Anonymous'; + } + const self = this; + this.img.onload = function () { + self.loaded = true; + }; + this.img.onerror = function () { + svg.log('ERROR: image "' + href + '" not found'); + self.loaded = true; + }; + this.img.src = href; + } else { + svg.ajax(href, true).then((img) => { + this.img = img; + this.loaded = true; + }); + } + } + renderChildren (ctx) { + const x = this.attribute('x').toPixels('x'); + const y = this.attribute('y').toPixels('y'); + + const width = this.attribute('width').toPixels('x'); + const height = this.attribute('height').toPixels('y'); + if (width === 0 || height === 0) return; + + ctx.save(); + if (this._isSvg) { + ctx.drawSvg(this.img, x, y, width, height); + } else { + ctx.translate(x, y); + svg.AspectRatio( + ctx, + this.attribute('preserveAspectRatio').value, + width, + this.img.width, + height, + this.img.height, + 0, + 0 + ); + ctx.drawImage(this.img, 0, 0); + } + ctx.restore(); + } + + getBoundingBox () { + const x = this.attribute('x').toPixels('x'); + const y = this.attribute('y').toPixels('y'); + const width = this.attribute('width').toPixels('x'); + const height = this.attribute('height').toPixels('y'); + return new svg.BoundingBox(x, y, x + width, y + height); + } + }; + + // group element + svg.Element.g = class extends svg.Element.RenderedElementBase { + getBoundingBox () { + const bb = new svg.BoundingBox(); + this.children.forEach((child) => { + bb.addBoundingBox(child.getBoundingBox()); + }); + return bb; + } + }; + + // symbol element + svg.Element.symbol = class extends svg.Element.RenderedElementBase { + render (ctx) { + // NO RENDER + } + }; + + // style element + svg.Element.style = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + // text, or spaces then CDATA + let css = ''; + [...node.childNodes].forEach(({nodeValue}) => { + css += nodeValue; + }); + css = css.replace(/(\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, ''); // remove comments + css = svg.compressSpaces(css); // replace whitespace + const cssDefs = css.split('}'); + cssDefs.forEach((cssDef) => { + if (svg.trim(cssDef) !== '') { + let [cssClasses, cssProps] = cssDef.split('{'); + cssClasses = cssClasses.split(','); + cssProps = cssProps.split(';'); + cssClasses.forEach((cssClass) => { + cssClass = svg.trim(cssClass); + if (cssClass !== '') { + const props = {}; + cssProps.forEach((cssProp) => { + const prop = cssProp.indexOf(':'); + const name = cssProp.substr(0, prop); + const value = cssProp.substr(prop + 1, cssProp.length - prop); + if (name != null && value != null) { + props[svg.trim(name)] = new svg.Property(svg.trim(name), svg.trim(value)); + } + }); + svg.Styles[cssClass] = props; + if (cssClass === '@font-face') { + const fontFamily = props['font-family'].value.replace(/"/g, ''); + const srcs = props['src'].value.split(','); + srcs.forEach((src) => { + if (src.includes('format("svg")')) { + const urlStart = src.indexOf('url'); + const urlEnd = src.indexOf(')', urlStart); + const url = src.substr(urlStart + 5, urlEnd - urlStart - 6); + // Can this ajax safely be converted to async? + const doc = svg.parseXml(svg.ajax(url)); + const fonts = doc.getElementsByTagName('font'); + [...fonts].forEach((font) => { + font = svg.CreateElement(font); + svg.Definitions[fontFamily] = font; + }); + } + }); + } + } + }); + } + }); + } + }; + + // use element + svg.Element.use = class extends svg.Element.RenderedElementBase { + constructor (node) { + super(node); + + this._el = this.getHrefAttribute().getDefinition(); + } + + setContext (ctx) { + super.setContext(ctx); + if (this.attribute('x').hasValue()) ctx.translate(this.attribute('x').toPixels('x'), 0); + if (this.attribute('y').hasValue()) ctx.translate(0, this.attribute('y').toPixels('y')); + } + + path (ctx) { + const {_el: element} = this; + if (element != null) element.path(ctx); + } + + getBoundingBox () { + const {_el: element} = this; + if (element != null) return element.getBoundingBox(); + } + + renderChildren (ctx) { + const {_el: element} = this; + if (element != null) { + let tempSvg = element; + if (element.type === 'symbol') { + // render me using a temporary svg element in symbol cases (https://www.w3.org/TR/SVG/struct.html#UseElement) + tempSvg = new svg.Element.svg(); + tempSvg.type = 'svg'; + tempSvg.attributes['viewBox'] = new svg.Property('viewBox', element.attribute('viewBox').value); + tempSvg.attributes['preserveAspectRatio'] = new svg.Property('preserveAspectRatio', element.attribute('preserveAspectRatio').value); + tempSvg.attributes['overflow'] = new svg.Property('overflow', element.attribute('overflow').value); + tempSvg.children = element.children; + } + if (tempSvg.type === 'svg') { + // if symbol or svg, inherit width/height from me + if (this.attribute('width').hasValue()) tempSvg.attributes['width'] = new svg.Property('width', this.attribute('width').value); + if (this.attribute('height').hasValue()) tempSvg.attributes['height'] = new svg.Property('height', this.attribute('height').value); + } + const oldParent = tempSvg.parent; + tempSvg.parent = null; + tempSvg.render(ctx); + tempSvg.parent = oldParent; + } + } + }; + + // mask element + svg.Element.mask = class extends svg.Element.ElementBase { + apply (ctx, element) { + // render as temp svg + let x = this.attribute('x').toPixels('x'); + let y = this.attribute('y').toPixels('y'); + let width = this.attribute('width').toPixels('x'); + let height = this.attribute('height').toPixels('y'); + + if (width === 0 && height === 0) { + const bb = new svg.BoundingBox(); + this.children.forEach((child) => { + bb.addBoundingBox(child.getBoundingBox()); + }); + x = Math.floor(bb.x1); + y = Math.floor(bb.y1); + width = Math.floor(bb.width()); + height = Math.floor(bb.height()); + } + + // temporarily remove mask to avoid recursion + const mask = element.attribute('mask').value; + element.attribute('mask').value = ''; + + const cMask = document.createElement('canvas'); + cMask.width = x + width; + cMask.height = y + height; + const maskCtx = cMask.getContext('2d'); + this.renderChildren(maskCtx); + + const c = document.createElement('canvas'); + c.width = x + width; + c.height = y + height; + const tempCtx = c.getContext('2d'); + element.render(tempCtx); + tempCtx.globalCompositeOperation = 'destination-in'; + tempCtx.fillStyle = maskCtx.createPattern(cMask, 'no-repeat'); + tempCtx.fillRect(0, 0, x + width, y + height); + + ctx.fillStyle = tempCtx.createPattern(c, 'no-repeat'); + ctx.fillRect(0, 0, x + width, y + height); + + // reassign mask + element.attribute('mask').value = mask; + } + + render (ctx) { + // NO RENDER + } + }; + + // clip element + svg.Element.clipPath = class extends svg.Element.ElementBase { + apply (ctx) { + this.children.forEach((child) => { + if (typeof child.path !== 'undefined') { + let transform = null; + if (child.attribute('transform').hasValue()) { + transform = new svg.Transform(child.attribute('transform').value); + transform.apply(ctx); + } + child.path(ctx); + ctx.clip(); + if (transform) { transform.unapply(ctx); } + } + }); + } + render (ctx) { + // NO RENDER + } + }; + + // filters + svg.Element.filter = class extends svg.Element.ElementBase { + apply (ctx, element) { + // render as temp svg + const bb = element.getBoundingBox(); + const x = Math.floor(bb.x1); + const y = Math.floor(bb.y1); + const width = Math.floor(bb.width()); + const height = Math.floor(bb.height()); + + // temporarily remove filter to avoid recursion + const filter = element.style('filter').value; + element.style('filter').value = ''; + + let px = 0, py = 0; + this.children.forEach((child) => { + const efd = child.extraFilterDistance || 0; + px = Math.max(px, efd); + py = Math.max(py, efd); + }); + + const c = document.createElement('canvas'); + c.width = width + 2 * px; + c.height = height + 2 * py; + const tempCtx = c.getContext('2d'); + tempCtx.translate(-x + px, -y + py); + element.render(tempCtx); + + // apply filters + this.children.forEach((child) => { + child.apply(tempCtx, 0, 0, width + 2 * px, height + 2 * py); + }); + + // render on me + ctx.drawImage(c, 0, 0, width + 2 * px, height + 2 * py, x - px, y - py, width + 2 * px, height + 2 * py); + + // reassign filter + element.style('filter', true).value = filter; + } + + render (ctx) { + // NO RENDER + } + }; + + svg.Element.feMorphology = class extends svg.Element.ElementBase { + apply (ctx, x, y, width, height) { + // TODO: implement + } + }; + + svg.Element.feComposite = class extends svg.Element.ElementBase { + apply (ctx, x, y, width, height) { + // TODO: implement + } + }; + + function imGet (img, x, y, width, height, rgba) { + return img[y * width * 4 + x * 4 + rgba]; + } + + function imSet (img, x, y, width, height, rgba, val) { + img[y * width * 4 + x * 4 + rgba] = val; + } + + svg.Element.feColorMatrix = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + let matrix = svg.ToNumberArray(this.attribute('values').value); + switch (this.attribute('type').valueOrDefault('matrix')) { // https://www.w3.org/TR/SVG/filters.html#feColorMatrixElement + case 'saturate': + const s = matrix[0]; + matrix = [ + 0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0, 0, + 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0, 0, + 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0, 0, + 0, 0, 0, 1, 0, + 0, 0, 0, 0, 1 + ]; + break; + case 'hueRotate': + const a = matrix[0] * Math.PI / 180.0; + const c = function (m1, m2, m3) { + return m1 + Math.cos(a) * m2 + Math.sin(a) * m3; + }; + matrix = [ + c(0.213, 0.787, -0.213), c(0.715, -0.715, -0.715), c(0.072, -0.072, 0.928), 0, 0, + c(0.213, -0.213, 0.143), c(0.715, 0.285, 0.140), c(0.072, -0.072, -0.283), 0, 0, + c(0.213, -0.213, -0.787), c(0.715, -0.715, 0.715), c(0.072, 0.928, 0.072), 0, 0, + 0, 0, 0, 1, 0, + 0, 0, 0, 0, 1 + ]; + break; + case 'luminanceToAlpha': + matrix = [ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0.2125, 0.7154, 0.0721, 0, 0, + 0, 0, 0, 0, 1 + ]; + break; + } + this.matrix = matrix; + + this._m = (i, v) => { + const mi = matrix[i]; + return mi * (mi < 0 ? v - 255 : v); + }; + } + apply (ctx, x, y, width, height) { + const {_m: m} = this; + // assuming x==0 && y==0 for now + const srcData = ctx.getImageData(0, 0, width, height); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const r = imGet(srcData.data, x, y, width, height, 0); + const g = imGet(srcData.data, x, y, width, height, 1); + const b = imGet(srcData.data, x, y, width, height, 2); + const a = imGet(srcData.data, x, y, width, height, 3); + imSet(srcData.data, x, y, width, height, 0, m(0, r) + m(1, g) + m(2, b) + m(3, a) + m(4, 1)); + imSet(srcData.data, x, y, width, height, 1, m(5, r) + m(6, g) + m(7, b) + m(8, a) + m(9, 1)); + imSet(srcData.data, x, y, width, height, 2, m(10, r) + m(11, g) + m(12, b) + m(13, a) + m(14, 1)); + imSet(srcData.data, x, y, width, height, 3, m(15, r) + m(16, g) + m(17, b) + m(18, a) + m(19, 1)); + } + } + ctx.clearRect(0, 0, width, height); + ctx.putImageData(srcData, 0, 0); + } + }; + + svg.Element.feGaussianBlur = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + this.blurRadius = Math.floor(this.attribute('stdDeviation').numValue()); + this.extraFilterDistance = this.blurRadius; + } + + apply (ctx, x, y, width, height) { + if (typeof canvasRGBA_ === 'undefined') { + svg.log('ERROR: `setStackBlurCanvasRGBA` must be run for blur to work'); + return; + } + + // Todo: This might not be a problem anymore with out `instanceof` fix + // StackBlur requires canvas be on document + ctx.canvas.id = svg.UniqueId(); + ctx.canvas.style.display = 'none'; + document.body.append(ctx.canvas); + canvasRGBA_(ctx.canvas, x, y, width, height, this.blurRadius); + ctx.canvas.remove(); + } + }; + + // title element, do nothing + svg.Element.title = class extends svg.Element.ElementBase { + constructor (node) { + super(); + } + }; + + // desc element, do nothing + svg.Element.desc = class extends svg.Element.ElementBase { + constructor (node) { + super(); + } + }; + + svg.Element.MISSING = class extends svg.Element.ElementBase { + constructor (node) { + super(); + svg.log('ERROR: Element \'' + node.nodeName + '\' not yet implemented.'); + } + }; + + // element factory + svg.CreateElement = function (node) { + const className = node.nodeName + .replace(/^[^:]+:/, '') // remove namespace + .replace(/-/g, ''); // remove dashes + let e; + if (typeof svg.Element[className] !== 'undefined') { + e = new svg.Element[className](node); + } else { + e = new svg.Element.MISSING(node); + } + + e.type = node.nodeName; + return e; + }; + + // load from url + svg.load = async function (ctx, url) { + const dom = await svg.ajax(url, true); + return svg.loadXml(ctx, dom); + }; + + // load from xml + svg.loadXml = function (ctx, xml) { + return svg.loadXmlDoc(ctx, svg.parseXml(xml)); + }; + + svg.loadXmlDoc = function (ctx, dom) { + svg.init(ctx); + + const mapXY = function (p) { + let e = ctx.canvas; + while (e) { + p.x -= e.offsetLeft; + p.y -= e.offsetTop; + e = e.offsetParent; + } + if (window.scrollX) p.x += window.scrollX; + if (window.scrollY) p.y += window.scrollY; + return p; + }; + + // bind mouse + if (svg.opts.ignoreMouse !== true) { + ctx.canvas.onclick = function (e) { + const args = e != null + ? [e.clientX, e.clientY] + : [event.clientX, event.clientY]; + const {x, y} = mapXY(new svg.Point(...args)); + svg.Mouse.onclick(x, y); + }; + ctx.canvas.onmousemove = function (e) { + const args = e != null + ? [e.clientX, e.clientY] + : [event.clientX, event.clientY]; + const {x, y} = mapXY(new svg.Point(...args)); + svg.Mouse.onmousemove(x, y); + }; + } + + const e = svg.CreateElement(dom.documentElement); + e.root = true; + + // render loop + let isFirstRender = true; + const draw = function (resolve) { + svg.ViewPort.Clear(); + if (ctx.canvas.parentNode) { + svg.ViewPort.SetCurrent( + ctx.canvas.parentNode.clientWidth, + ctx.canvas.parentNode.clientHeight + ); + } + + if (svg.opts.ignoreDimensions !== true) { + // set canvas size + if (e.style('width').hasValue()) { + ctx.canvas.width = e.style('width').toPixels('x'); + ctx.canvas.style.width = ctx.canvas.width + 'px'; + } + if (e.style('height').hasValue()) { + ctx.canvas.height = e.style('height').toPixels('y'); + ctx.canvas.style.height = ctx.canvas.height + 'px'; + } + } + let cWidth = ctx.canvas.clientWidth || ctx.canvas.width; + let cHeight = ctx.canvas.clientHeight || ctx.canvas.height; + if (svg.opts.ignoreDimensions === true && + e.style('width').hasValue() && e.style('height').hasValue() + ) { + cWidth = e.style('width').toPixels('x'); + cHeight = e.style('height').toPixels('y'); + } + svg.ViewPort.SetCurrent(cWidth, cHeight); + + if (svg.opts.offsetX != null) { + e.attribute('x', true).value = svg.opts.offsetX; + } + if (svg.opts.offsetY != null) { + e.attribute('y', true).value = svg.opts.offsetY; + } + if (svg.opts.scaleWidth != null || svg.opts.scaleHeight != null) { + const viewBox = svg.ToNumberArray(e.attribute('viewBox').value); + let xRatio = null, yRatio = null; + + if (svg.opts.scaleWidth != null) { + if (e.attribute('width').hasValue()) { + xRatio = e.attribute('width').toPixels('x') / svg.opts.scaleWidth; + } else if (!isNaN(viewBox[2])) { + xRatio = viewBox[2] / svg.opts.scaleWidth; + } + } + + if (svg.opts.scaleHeight != null) { + if (e.attribute('height').hasValue()) { + yRatio = e.attribute('height').toPixels('y') / svg.opts.scaleHeight; + } else if (!isNaN(viewBox[3])) { + yRatio = viewBox[3] / svg.opts.scaleHeight; + } + } + + if (xRatio == null) { xRatio = yRatio; } + if (yRatio == null) { yRatio = xRatio; } + + e.attribute('width', true).value = svg.opts.scaleWidth; + e.attribute('height', true).value = svg.opts.scaleHeight; + e.attribute('viewBox', true).value = '0 0 ' + (cWidth * xRatio) + ' ' + (cHeight * yRatio); + e.attribute('preserveAspectRatio', true).value = 'none'; + } + + // clear and render + if (svg.opts.ignoreClear !== true) { + ctx.clearRect(0, 0, cWidth, cHeight); + } + e.render(ctx); + if (isFirstRender) { + isFirstRender = false; + resolve(dom); + } + }; + + let waitingForImages = true; + svg.intervalID = setInterval(function () { + let needUpdate = false; + + if (waitingForImages && svg.ImagesLoaded()) { + waitingForImages = false; + needUpdate = true; + } + + // need update from mouse events? + if (svg.opts.ignoreMouse !== true) { + needUpdate = needUpdate | svg.Mouse.hasEvents(); + } + + // need update from animations? + if (svg.opts.ignoreAnimation !== true) { + svg.Animations.forEach((animation) => { + needUpdate = needUpdate | animation.update(1000 / svg.FRAMERATE); + }); + } + + // need update from redraw? + if (typeof svg.opts.forceRedraw === 'function') { + if (svg.opts.forceRedraw() === true) { + needUpdate = true; + } + } + + // render if needed + if (needUpdate) { + draw(); + svg.Mouse.runEvents(); // run and clear our events + } + }, 1000 / svg.FRAMERATE); + return new Promise((resolve, reject) => { + if (svg.ImagesLoaded()) { + waitingForImages = false; + draw(resolve); + } + }); + }; + + svg.stop = () => { + if (svg.intervalID) { + clearInterval(svg.intervalID); + } + }; + + svg.Mouse = { + events: [], + hasEvents () { return this.events.length !== 0; }, + + onclick (x, y) { + this.events.push({ + type: 'onclick', x, y, + run (e) { if (e.onclick) e.onclick(); } + }); + }, + + onmousemove (x, y) { + this.events.push({ + type: 'onmousemove', x, y, + run (e) { if (e.onmousemove) e.onmousemove(); } + }); + }, + + eventElements: [], + + checkPath (element, ctx) { + this.events.forEach(({x, y}, i) => { + if (ctx.isPointInPath && ctx.isPointInPath(x, y)) { + this.eventElements[i] = element; + } + }); + }, + + checkBoundingBox (element, bb) { + this.events.forEach(({x, y}, i) => { + if (bb.isPointInBox(x, y)) { + this.eventElements[i] = element; + } + }); + }, + + runEvents () { + svg.ctx.canvas.style.cursor = ''; + + this.events.forEach((e, i) => { + let element = this.eventElements[i]; + while (element) { + e.run(element); + element = element.parent; + } + }); + + // done running, clear + this.events = []; + this.eventElements = []; + } + }; + + return svg; +} + +if (typeof CanvasRenderingContext2D !== 'undefined') { + CanvasRenderingContext2D.prototype.drawSvg = function (s, dx, dy, dw, dh) { + canvg(this.canvas, s, { + ignoreMouse: true, + ignoreAnimation: true, + ignoreDimensions: true, + ignoreClear: true, + offsetX: dx, + offsetY: dy, + scaleWidth: dw, + scaleHeight: dh + }); + }; +} diff --git a/editor/canvg/rgbcolor.js b/editor/canvg/rgbcolor.js index 4b44a635..df5f2954 100644 --- a/editor/canvg/rgbcolor.js +++ b/editor/canvg/rgbcolor.js @@ -1,294 +1,292 @@ -/*jslint vars: true*/ +/** + * For parsing color values + * @module RGBColor + * @author Stoyan Stefanov + * @see https://www.phpied.com/rgb-color-parser-in-javascript/ + * @license MIT +*/ +const simpleColors = { + aliceblue: 'f0f8ff', + antiquewhite: 'faebd7', + aqua: '00ffff', + aquamarine: '7fffd4', + azure: 'f0ffff', + beige: 'f5f5dc', + bisque: 'ffe4c4', + black: '000000', + blanchedalmond: 'ffebcd', + blue: '0000ff', + blueviolet: '8a2be2', + brown: 'a52a2a', + burlywood: 'deb887', + cadetblue: '5f9ea0', + chartreuse: '7fff00', + chocolate: 'd2691e', + coral: 'ff7f50', + cornflowerblue: '6495ed', + cornsilk: 'fff8dc', + crimson: 'dc143c', + cyan: '00ffff', + darkblue: '00008b', + darkcyan: '008b8b', + darkgoldenrod: 'b8860b', + darkgray: 'a9a9a9', + darkgreen: '006400', + darkkhaki: 'bdb76b', + darkmagenta: '8b008b', + darkolivegreen: '556b2f', + darkorange: 'ff8c00', + darkorchid: '9932cc', + darkred: '8b0000', + darksalmon: 'e9967a', + darkseagreen: '8fbc8f', + darkslateblue: '483d8b', + darkslategray: '2f4f4f', + darkturquoise: '00ced1', + darkviolet: '9400d3', + deeppink: 'ff1493', + deepskyblue: '00bfff', + dimgray: '696969', + dodgerblue: '1e90ff', + feldspar: 'd19275', + firebrick: 'b22222', + floralwhite: 'fffaf0', + forestgreen: '228b22', + fuchsia: 'ff00ff', + gainsboro: 'dcdcdc', + ghostwhite: 'f8f8ff', + gold: 'ffd700', + goldenrod: 'daa520', + gray: '808080', + green: '008000', + greenyellow: 'adff2f', + honeydew: 'f0fff0', + hotpink: 'ff69b4', + indianred: 'cd5c5c', + indigo: '4b0082', + ivory: 'fffff0', + khaki: 'f0e68c', + lavender: 'e6e6fa', + lavenderblush: 'fff0f5', + lawngreen: '7cfc00', + lemonchiffon: 'fffacd', + lightblue: 'add8e6', + lightcoral: 'f08080', + lightcyan: 'e0ffff', + lightgoldenrodyellow: 'fafad2', + lightgrey: 'd3d3d3', + lightgreen: '90ee90', + lightpink: 'ffb6c1', + lightsalmon: 'ffa07a', + lightseagreen: '20b2aa', + lightskyblue: '87cefa', + lightslateblue: '8470ff', + lightslategray: '778899', + lightsteelblue: 'b0c4de', + lightyellow: 'ffffe0', + lime: '00ff00', + limegreen: '32cd32', + linen: 'faf0e6', + magenta: 'ff00ff', + maroon: '800000', + mediumaquamarine: '66cdaa', + mediumblue: '0000cd', + mediumorchid: 'ba55d3', + mediumpurple: '9370d8', + mediumseagreen: '3cb371', + mediumslateblue: '7b68ee', + mediumspringgreen: '00fa9a', + mediumturquoise: '48d1cc', + mediumvioletred: 'c71585', + midnightblue: '191970', + mintcream: 'f5fffa', + mistyrose: 'ffe4e1', + moccasin: 'ffe4b5', + navajowhite: 'ffdead', + navy: '000080', + oldlace: 'fdf5e6', + olive: '808000', + olivedrab: '6b8e23', + orange: 'ffa500', + orangered: 'ff4500', + orchid: 'da70d6', + palegoldenrod: 'eee8aa', + palegreen: '98fb98', + paleturquoise: 'afeeee', + palevioletred: 'd87093', + papayawhip: 'ffefd5', + peachpuff: 'ffdab9', + peru: 'cd853f', + pink: 'ffc0cb', + plum: 'dda0dd', + powderblue: 'b0e0e6', + purple: '800080', + red: 'ff0000', + rosybrown: 'bc8f8f', + royalblue: '4169e1', + saddlebrown: '8b4513', + salmon: 'fa8072', + sandybrown: 'f4a460', + seagreen: '2e8b57', + seashell: 'fff5ee', + sienna: 'a0522d', + silver: 'c0c0c0', + skyblue: '87ceeb', + slateblue: '6a5acd', + slategray: '708090', + snow: 'fffafa', + springgreen: '00ff7f', + steelblue: '4682b4', + tan: 'd2b48c', + teal: '008080', + thistle: 'd8bfd8', + tomato: 'ff6347', + turquoise: '40e0d0', + violet: 'ee82ee', + violetred: 'd02090', + wheat: 'f5deb3', + white: 'ffffff', + whitesmoke: 'f5f5f5', + yellow: 'ffff00', + yellowgreen: '9acd32' +}; + +// array of color definition objects +const colorDefs = [ + { + re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, + example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], + process (bits) { + return [ + parseInt(bits[1], 10), + parseInt(bits[2], 10), + parseInt(bits[3], 10) + ]; + } + }, + { + re: /^(\w{2})(\w{2})(\w{2})$/, + example: ['#00ff00', '336699'], + process (bits) { + return [ + parseInt(bits[1], 16), + parseInt(bits[2], 16), + parseInt(bits[3], 16) + ]; + } + }, + { + re: /^(\w{1})(\w{1})(\w{1})$/, + example: ['#fb0', 'f0f'], + process (bits) { + return [ + parseInt(bits[1] + bits[1], 16), + parseInt(bits[2] + bits[2], 16), + parseInt(bits[3] + bits[3], 16) + ]; + } + } +]; + /** * A class to parse color values - * @author Stoyan Stefanov - * @link http://www.phpied.com/rgb-color-parser-in-javascript/ - * @license Use it if you like it */ -function RGBColor(color_string) { 'use strict'; +export default class RGBColor { + /** + * @param {string} colorString + */ + constructor (colorString) { this.ok = false; // strip any leading # - if (color_string.charAt(0) === '#') { // remove # if any - color_string = color_string.substr(1,6); + if (colorString.charAt(0) === '#') { // remove # if any + colorString = colorString.substr(1, 6); } - color_string = color_string.replace(/ /g,''); - color_string = color_string.toLowerCase(); + colorString = colorString.replace(/ /g, ''); + colorString = colorString.toLowerCase(); // before getting into regexps, try simple matches // and overwrite the input - var simple_colors = { - aliceblue: 'f0f8ff', - antiquewhite: 'faebd7', - aqua: '00ffff', - aquamarine: '7fffd4', - azure: 'f0ffff', - beige: 'f5f5dc', - bisque: 'ffe4c4', - black: '000000', - blanchedalmond: 'ffebcd', - blue: '0000ff', - blueviolet: '8a2be2', - brown: 'a52a2a', - burlywood: 'deb887', - cadetblue: '5f9ea0', - chartreuse: '7fff00', - chocolate: 'd2691e', - coral: 'ff7f50', - cornflowerblue: '6495ed', - cornsilk: 'fff8dc', - crimson: 'dc143c', - cyan: '00ffff', - darkblue: '00008b', - darkcyan: '008b8b', - darkgoldenrod: 'b8860b', - darkgray: 'a9a9a9', - darkgreen: '006400', - darkkhaki: 'bdb76b', - darkmagenta: '8b008b', - darkolivegreen: '556b2f', - darkorange: 'ff8c00', - darkorchid: '9932cc', - darkred: '8b0000', - darksalmon: 'e9967a', - darkseagreen: '8fbc8f', - darkslateblue: '483d8b', - darkslategray: '2f4f4f', - darkturquoise: '00ced1', - darkviolet: '9400d3', - deeppink: 'ff1493', - deepskyblue: '00bfff', - dimgray: '696969', - dodgerblue: '1e90ff', - feldspar: 'd19275', - firebrick: 'b22222', - floralwhite: 'fffaf0', - forestgreen: '228b22', - fuchsia: 'ff00ff', - gainsboro: 'dcdcdc', - ghostwhite: 'f8f8ff', - gold: 'ffd700', - goldenrod: 'daa520', - gray: '808080', - green: '008000', - greenyellow: 'adff2f', - honeydew: 'f0fff0', - hotpink: 'ff69b4', - indianred : 'cd5c5c', - indigo : '4b0082', - ivory: 'fffff0', - khaki: 'f0e68c', - lavender: 'e6e6fa', - lavenderblush: 'fff0f5', - lawngreen: '7cfc00', - lemonchiffon: 'fffacd', - lightblue: 'add8e6', - lightcoral: 'f08080', - lightcyan: 'e0ffff', - lightgoldenrodyellow: 'fafad2', - lightgrey: 'd3d3d3', - lightgreen: '90ee90', - lightpink: 'ffb6c1', - lightsalmon: 'ffa07a', - lightseagreen: '20b2aa', - lightskyblue: '87cefa', - lightslateblue: '8470ff', - lightslategray: '778899', - lightsteelblue: 'b0c4de', - lightyellow: 'ffffe0', - lime: '00ff00', - limegreen: '32cd32', - linen: 'faf0e6', - magenta: 'ff00ff', - maroon: '800000', - mediumaquamarine: '66cdaa', - mediumblue: '0000cd', - mediumorchid: 'ba55d3', - mediumpurple: '9370d8', - mediumseagreen: '3cb371', - mediumslateblue: '7b68ee', - mediumspringgreen: '00fa9a', - mediumturquoise: '48d1cc', - mediumvioletred: 'c71585', - midnightblue: '191970', - mintcream: 'f5fffa', - mistyrose: 'ffe4e1', - moccasin: 'ffe4b5', - navajowhite: 'ffdead', - navy: '000080', - oldlace: 'fdf5e6', - olive: '808000', - olivedrab: '6b8e23', - orange: 'ffa500', - orangered: 'ff4500', - orchid: 'da70d6', - palegoldenrod: 'eee8aa', - palegreen: '98fb98', - paleturquoise: 'afeeee', - palevioletred: 'd87093', - papayawhip: 'ffefd5', - peachpuff: 'ffdab9', - peru: 'cd853f', - pink: 'ffc0cb', - plum: 'dda0dd', - powderblue: 'b0e0e6', - purple: '800080', - red: 'ff0000', - rosybrown: 'bc8f8f', - royalblue: '4169e1', - saddlebrown: '8b4513', - salmon: 'fa8072', - sandybrown: 'f4a460', - seagreen: '2e8b57', - seashell: 'fff5ee', - sienna: 'a0522d', - silver: 'c0c0c0', - skyblue: '87ceeb', - slateblue: '6a5acd', - slategray: '708090', - snow: 'fffafa', - springgreen: '00ff7f', - steelblue: '4682b4', - tan: 'd2b48c', - teal: '008080', - thistle: 'd8bfd8', - tomato: 'ff6347', - turquoise: '40e0d0', - violet: 'ee82ee', - violetred: 'd02090', - wheat: 'f5deb3', - white: 'ffffff', - whitesmoke: 'f5f5f5', - yellow: 'ffff00', - yellowgreen: '9acd32' - }; - var key; - for (key in simple_colors) { - if (simple_colors.hasOwnProperty(key)) { - if (color_string == key) { - color_string = simple_colors[key]; - } - } + if (colorString in simpleColors) { + colorString = simpleColors[colorString]; } - // emd of simple type-in colors + // end of simple type-in colors - // array of color definition objects - var color_defs = [ - { - re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, - example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], - process: function (bits){ - return [ - parseInt(bits[1], 10), - parseInt(bits[2], 10), - parseInt(bits[3], 10) - ]; - } - }, - { - re: /^(\w{2})(\w{2})(\w{2})$/, - example: ['#00ff00', '336699'], - process: function (bits){ - return [ - parseInt(bits[1], 16), - parseInt(bits[2], 16), - parseInt(bits[3], 16) - ]; - } - }, - { - re: /^(\w{1})(\w{1})(\w{1})$/, - example: ['#fb0', 'f0f'], - process: function (bits){ - return [ - parseInt(bits[1] + bits[1], 16), - parseInt(bits[2] + bits[2], 16), - parseInt(bits[3] + bits[3], 16) - ]; - } - } - ]; - - var i; // search through the definitions to find a match - for (i = 0; i < color_defs.length; i++) { - var re = color_defs[i].re; - var processor = color_defs[i].process; - var bits = re.exec(color_string); - if (bits) { - var channels = processor(bits); - this.r = channels[0]; - this.g = channels[1]; - this.b = channels[2]; - this.ok = true; - } - + for (let i = 0; i < colorDefs.length; i++) { + const {re} = colorDefs[i]; + const processor = colorDefs[i].process; + const bits = re.exec(colorString); + if (bits) { + const [r, g, b] = processor(bits); + Object.assign(this, {r, g, b}); + this.ok = true; + } } // validate/cleanup values this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r); this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g); this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b); + } - // some getters - this.toRGB = function () { - return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; - }; - this.toHex = function () { - var r = this.r.toString(16); - var g = this.g.toString(16); - var b = this.b.toString(16); - if (r.length === 1) {r = '0' + r;} - if (g.length === 1) {g = '0' + g;} - if (b.length === 1) {b = '0' + b;} - return '#' + r + g + b; - }; + // some getters + /** + * @returns {string} + */ + toRGB () { + return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; + } - // help - this.getHelpXML = function () { - var i, j; - var examples = []; - // add regexps - for (i = 0; i < color_defs.length; i++) { - var example = color_defs[i].example; - for (j = 0; j < example.length; j++) { - examples[examples.length] = example[j]; - } - } - // add type-in colors - var sc; - for (sc in simple_colors) { - if (simple_colors.hasOwnProperty(sc)) { - examples[examples.length] = sc; - } - } + /** + * @returns {string} + */ + toHex () { + let r = this.r.toString(16); + let g = this.g.toString(16); + let b = this.b.toString(16); + if (r.length === 1) { r = '0' + r; } + if (g.length === 1) { g = '0' + g; } + if (b.length === 1) { b = '0' + b; } + return '#' + r + g + b; + } - var xml = document.createElement('ul'); - xml.setAttribute('id', 'rgbcolor-examples'); - for (i = 0; i < examples.length; i++) { - try { - var list_item = document.createElement('li'); - var list_color = new RGBColor(examples[i]); - var example_div = document.createElement('div'); - example_div.style.cssText = - 'margin: 3px; ' - + 'border: 1px solid black; ' - + 'background:' + list_color.toHex() + '; ' - + 'color:' + list_color.toHex() - ; - example_div.appendChild(document.createTextNode('test')); - var list_item_value = document.createTextNode( - ' ' + examples[i] + ' -> ' + list_color.toRGB() + ' -> ' + list_color.toHex() - ); - list_item.appendChild(example_div); - list_item.appendChild(list_item_value); - xml.appendChild(list_item); - - } catch(e){} - } - return xml; - - }; + /** + * help + * @returns {HTMLUListElement} + */ + getHelpXML () { + const examples = []; + // add regexps + for (let i = 0; i < colorDefs.length; i++) { + const {example} = colorDefs[i]; + for (let j = 0; j < example.length; j++) { + examples[examples.length] = example[j]; + } + } + // add type-in colors + examples.push(...Object.keys(simpleColors)); + const xml = document.createElement('ul'); + xml.setAttribute('id', 'rgbcolor-examples'); + for (let i = 0; i < examples.length; i++) { + try { + const listItem = document.createElement('li'); + const listColor = new RGBColor(examples[i]); + const exampleDiv = document.createElement('div'); + exampleDiv.style.cssText = +`margin: 3px; +border: 1px solid black; +background: ${listColor.toHex()}; +color: ${listColor.toHex()};` + ; + exampleDiv.append('test'); + const listItemValue = ` ${examples[i]} -> ${listColor.toRGB()} -> ${listColor.toHex()}`; + listItem.append(exampleDiv, listItemValue); + xml.append(listItem); + } catch (e) {} + } + return xml; + } } diff --git a/editor/config-sample.js b/editor/config-sample.js deleted file mode 100644 index 0336b243..00000000 --- a/editor/config-sample.js +++ /dev/null @@ -1,146 +0,0 @@ -// DO NOT EDIT THIS FILE! -// THIS FILE IS JUST A SAMPLE; TO APPLY, YOU MUST -// CREATE A NEW FILE config.js AND ADD CONTENTS -// SUCH AS SHOWN BELOW INTO THAT FILE. - -/*globals svgEditor*/ -/* -The config.js file is intended for the setting of configuration or - preferences which must run early on; if this is not needed, it is - recommended that you create an extension instead (for greater - reusability and modularity). -*/ - -// CONFIG AND EXTENSION SETTING -/* -See defaultConfig and defaultExtensions in svg-editor.js for a list - of possible configuration settings. - -See svg-editor.js for documentation on using setConfig(). -*/ - -// URL OVERRIDE CONFIG -svgEditor.setConfig({ - /** - To override the ability for URLs to set URL-based SVG content, - uncomment the following: - */ - // preventURLContentLoading: true, - /** - To override the ability for URLs to set other configuration (including - extension config), uncomment the following: - */ - // preventAllURLConfig: true, - /** - To override the ability for URLs to set their own extensions, - uncomment the following (note that if setConfig() is used in - extension code, it will still be additive to extensions, - however): - */ - // lockExtensions: true, -}); - -svgEditor.setConfig({ - /* - Provide default values here which differ from that of the editor but - which the URL can override - */ -}, {allowInitialUserOverride: true}); - -// EXTENSION CONFIG -svgEditor.setConfig({ - extensions: [ - // 'ext-overview_window.js', 'ext-markers.js', 'ext-connector.js', 'ext-eyedropper.js', 'ext-shapes.js', 'ext-imagelib.js', 'ext-grid.js', 'ext-polygon.js', 'ext-star.js', 'ext-panning.js', 'ext-storage.js' - ] - // , noDefaultExtensions: false, // noDefaultExtensions can only be meaningfully used in config.js or in the URL -}); - -// OTHER CONFIG -svgEditor.setConfig({ - // canvasName: 'default', - // canvas_expansion: 3, - // initFill: { - // color: 'FF0000', // solid red - // opacity: 1 - // }, - // initStroke: { - // width: 5, - // color: '000000', // solid black - // opacity: 1 - // }, - // initOpacity: 1, - // colorPickerCSS: null, - // initTool: 'select', - // exportWindowType: 'new', // 'same' - // wireframe: false, - // showlayers: false, - // no_save_warning: false, - // PATH CONFIGURATION - // imgPath: 'images/', - // langPath: 'locale/', - // extPath: 'extensions/', - // jGraduatePath: 'jgraduate/images/', - /* - Uncomment the following to allow at least same domain (embedded) access, - including file:// access. - Setting as `['*']` would allow any domain to access but would be unsafe to - data privacy and integrity. - */ - // allowedOrigins: [window.location.origin || 'null'], // May be 'null' (as a string) when used as a file:// URL - // DOCUMENT PROPERTIES - // dimensions: [640, 480], - // EDITOR OPTIONS - // gridSnapping: false, - // gridColor: '#000', - // baseUnit: 'px', - // snappingStep: 10, - // showRulers: true, - // EXTENSION-RELATED (GRID) - // showGrid: false, // Set by ext-grid.js - // EXTENSION-RELATED (STORAGE) - // noStorageOnLoad: false, // Some interaction with ext-storage.js; prevent even the loading of previously saved local storage - // forceStorage: false, // Some interaction with ext-storage.js; strongly discouraged from modification as it bypasses user privacy by preventing them from choosing whether to keep local storage or not - // emptyStorageOnDecline: true, // Used by ext-storage.js; empty any prior storage if the user declines to store -}); - -// PREF CHANGES -/** -setConfig() can also be used to set preferences in addition to - configuration (see defaultPrefs in svg-editor.js for a list of - possible settings), but at least if you are using ext-storage.js - to store preferences, it will probably be better to let your - users control these. -As with configuration, one may use allowInitialUserOverride, but - in the case of preferences, any previously stored preferences - will also thereby be enabled to override this setting (and at a - higher priority than any URL preference setting overrides). - Failing to use allowInitialUserOverride will ensure preferences - are hard-coded here regardless of URL or prior user storage setting. -*/ -svgEditor.setConfig( - { - // lang: '', // Set dynamically within locale.js if not previously set - // iconsize: '', // Will default to 's' if the window height is smaller than the minimum height and 'm' otherwise - /** - * When showing the preferences dialog, svg-editor.js currently relies - * on curPrefs instead of $.pref, so allowing an override for bkgd_color - * means that this value won't have priority over block auto-detection as - * far as determining which color shows initially in the preferences - * dialog (though it can be changed and saved). - */ - // bkgd_color: '#FFF', - // bkgd_url: '', - // img_save: 'embed', - // Only shows in UI as far as alert notices - // save_notice_done: false, - // export_notice_done: false - } -); -svgEditor.setConfig( - { - // Indicate pref settings here if you wish to allow user storage or URL settings - // to be able to override your default preferences (unless other config options - // have already explicitly prevented one or the other) - }, - {allowInitialUserOverride: true} -); diff --git a/editor/contextmenu.js b/editor/contextmenu.js index 9fca31d8..f039a49a 100644 --- a/editor/contextmenu.js +++ b/editor/contextmenu.js @@ -1,66 +1,104 @@ -/*globals $, svgEditor*/ -/*jslint vars: true, eqeq: true*/ +/* globals jQuery */ /** - * Package: svgedit.contextmenu - * - * Licensed under the Apache License, Version 2 - * - * Author: Adam Bender + * Adds context menu functionality + * @module contextmenu + * @license Apache-2.0 + * @author Adam Bender */ // Dependencies: // 1) jQuery (for dom injection of context menus) -var svgedit = svgedit || {}; -(function() { - var self = this; - if (!svgedit.contextmenu) { - svgedit.contextmenu = {}; - } - self.contextMenuExtensions = {}; - var menuItemIsValid = function(menuItem) { - return menuItem && menuItem.id && menuItem.label && menuItem.action && typeof menuItem.action == 'function'; - }; - var addContextMenuItem = function(menuItem) { - // menuItem: {id, label, shortcut, action} - if (!menuItemIsValid(menuItem)) { - console.error("Menu items must be defined and have at least properties: id, label, action, where action must be a function"); - return; - } - if (menuItem.id in self.contextMenuExtensions) { - console.error('Cannot add extension "' + menuItem.id + '", an extension by that name already exists"'); - return; - } - // Register menuItem action, see below for deferred menu dom injection - console.log("Registed contextmenu item: {id:"+ menuItem.id+", label:"+menuItem.label+"}"); - self.contextMenuExtensions[menuItem.id] = menuItem; - //TODO: Need to consider how to handle custom enable/disable behavior - }; - var hasCustomHandler = function(handlerKey) { - return self.contextMenuExtensions[handlerKey] && true; - }; - var getCustomHandler = function(handlerKey) { - return self.contextMenuExtensions[handlerKey].action; - }; - var injectExtendedContextMenuItemIntoDom = function(menuItem) { - if (Object.keys(self.contextMenuExtensions).length === 0) { - // all menuItems appear at the bottom of the menu in their own container. - // if this is the first extension menu we need to add the separator. - $("#cmenu_canvas").append("
  • "); - } - var shortcut = menuItem.shortcut || ""; - $("#cmenu_canvas").append("
  • " - + menuItem.label + "" - + shortcut + "
  • "); - }; - // Defer injection to wait out initial menu processing. This probably goes away once all context - // menu behavior is brought here. - svgEditor.ready(function() { - var menuItem; - for (menuItem in contextMenuExtensions) { - injectExtendedContextMenuItemIntoDom(contextMenuExtensions[menuItem]); - } - }); - svgedit.contextmenu.resetCustomMenus = function(){self.contextMenuExtensions = {};}; - svgedit.contextmenu.add = addContextMenuItem; - svgedit.contextmenu.hasCustomHandler = hasCustomHandler; - svgedit.contextmenu.getCustomHandler = getCustomHandler; -}()); + +const $ = jQuery; + +let contextMenuExtensions = {}; + +/** + * Signature depends on what the user adds; in the case of our uses with + * SVGEditor, no parameters are passed nor anything expected for a return. + * @callback module:contextmenu.MenuItemAction +*/ + +/** +* @typedef {PlainObject} module:contextmenu.MenuItem +* @property {string} id +* @property {string} label +* @property {module:contextmenu.MenuItemAction} action +*/ + +/** +* @param {module:contextmenu.MenuItem} menuItem +* @returns {boolean} +*/ +const menuItemIsValid = function (menuItem) { + return menuItem && menuItem.id && menuItem.label && menuItem.action && typeof menuItem.action === 'function'; +}; + +/** +* @function module:contextmenu.add +* @param {module:contextmenu.MenuItem} menuItem +* @returns {undefined} +*/ +export const add = function (menuItem) { + // menuItem: {id, label, shortcut, action} + if (!menuItemIsValid(menuItem)) { + console.error('Menu items must be defined and have at least properties: id, label, action, where action must be a function'); + return; + } + if (menuItem.id in contextMenuExtensions) { + console.error('Cannot add extension "' + menuItem.id + '", an extension by that name already exists"'); + return; + } + // Register menuItem action, see below for deferred menu dom injection + console.log('Registed contextmenu item: {id:' + menuItem.id + ', label:' + menuItem.label + '}'); + contextMenuExtensions[menuItem.id] = menuItem; + // TODO: Need to consider how to handle custom enable/disable behavior +}; + +/** +* @function module:contextmenu.hasCustomHandler +* @param {string} handlerKey +* @returns {boolean} +*/ +export const hasCustomHandler = function (handlerKey) { + return Boolean(contextMenuExtensions[handlerKey]); +}; + +/** +* @function module:contextmenu.getCustomHandler +* @param {string} handlerKey +* @returns {module:contextmenu.MenuItemAction} +*/ +export const getCustomHandler = function (handlerKey) { + return contextMenuExtensions[handlerKey].action; +}; + +/** +* @param {module:contextmenu.MenuItem} menuItem +* @returns {undefined} +*/ +const injectExtendedContextMenuItemIntoDom = function (menuItem) { + if (!Object.keys(contextMenuExtensions).length) { + // all menuItems appear at the bottom of the menu in their own container. + // if this is the first extension menu we need to add the separator. + $('#cmenu_canvas').append("
  • "); + } + const shortcut = menuItem.shortcut || ''; + $('#cmenu_canvas').append("
  • " + + menuItem.label + "" + + shortcut + '
  • '); +}; + +/** +* @function module:contextmenu.injectExtendedContextMenuItemsIntoDom +* @returns {undefined} +*/ +export const injectExtendedContextMenuItemsIntoDom = function () { + for (const menuItem in contextMenuExtensions) { + injectExtendedContextMenuItemIntoDom(contextMenuExtensions[menuItem]); + } +}; +/** +* @function module:contextmenu.resetCustomMenus +* @returns {undefined} +*/ +export const resetCustomMenus = function () { contextMenuExtensions = {}; }; diff --git a/editor/contextmenu/jQuery.contextMenu.js b/editor/contextmenu/jQuery.contextMenu.js new file mode 100755 index 00000000..2f65b0e4 --- /dev/null +++ b/editor/contextmenu/jQuery.contextMenu.js @@ -0,0 +1,259 @@ +/** + * jQuery Context Menu Plugin + * Cory S.N. LaViska + * A Beautiful Site ({@link https://abeautifulsite.net/}) + * Modified by Alexis Deveria + * + * More info: {@link https://abeautifulsite.net/2008/09/jquery-context-menu-plugin/} + * + * @module jQueryContextMenu + * @todo Update to latest version and adapt (and needs jQuery update as well): {@link https://github.com/swisnl/jQuery-contextMenu} + * @version 1.01 + * + * @license + * Terms of Use + * + * This plugin is dual-licensed under the GNU General Public License + * and the MIT License and is copyright A Beautiful Site, LLC. + * +*/ +import {isMac} from '../browser.js'; + +/** +* @callback module:jQueryContextMenu.jQueryContextMenuCallback +* @param {string} href The `href` value after the first character (for bypassing an initial `#`) +* @param {external:jQuery} srcElement The wrapped jQuery srcElement +* @param {{x: Float, y: Float, docX: Float, docY: Float}} coords +*/ + +/** +* @typedef {PlainObject} module:jQueryContextMenu.jQueryContextMenuConfig +* @property {string} menu +* @property {Float} inSpeed +* @property {Float} outSpeed +* @property {boolean} allowLeft +*/ + +/** +* Adds {@link external:jQuery.fn.contextMenu}, {@link external:jQuery.fn.disableContextMenuItems}, {@link external:jQuery.fn.enableContextMenuItems}, {@link external:jQuery.fn.disableContextMenu}, {@link external:jQuery.fn.enableContextMenu}, {@link external:jQuery.fn.destroyContextMenu} +* @function module:jQueryContextMenu.jQueryContextMenu +* @param {external:jQuery} $ The jQuery object to wrap (with `contextMenu`, `disableContextMenuItems`, `enableContextMenuItems`, `disableContextMenu`, `enableContextMenu`, `destroyContextMenu`) +* @returns {external:jQuery} +*/ +function jQueryContextMenu ($) { + const win = $(window); + const doc = $(document); + + $.extend($.fn, { + /** + * @memberof external:jQuery.fn + * @param {module:jQueryContextMenu.jQueryContextMenuConfig} o + * @param {module:jQueryContextMenu.jQueryContextMenuCallback} callback + * @returns {external:jQuery} + */ + contextMenu (o, callback) { + // Defaults + if (o.menu === undefined) return false; + if (o.inSpeed === undefined) o.inSpeed = 150; + if (o.outSpeed === undefined) o.outSpeed = 75; + // 0 needs to be -1 for expected results (no fade) + if (o.inSpeed === 0) o.inSpeed = -1; + if (o.outSpeed === 0) o.outSpeed = -1; + // Loop each context menu + $(this).each(function () { + const el = $(this); + const offset = $(el).offset(); + + const menu = $('#' + o.menu); + + // Add contextMenu class + menu.addClass('contextMenu'); + // Simulate a true right click + $(this).bind('mousedown', function (e) { + const evt = e; + $(this).mouseup(function (e) { + const srcElement = $(this); + srcElement.unbind('mouseup'); + if (evt.button === 2 || o.allowLeft || + (evt.ctrlKey && isMac())) { + e.stopPropagation(); + // Hide context menus that may be showing + $('.contextMenu').hide(); + // Get this context menu + + if (el.hasClass('disabled')) return false; + + // Detect mouse position + let x = e.pageX, y = e.pageY; + + const xOff = win.width() - menu.width(), + yOff = win.height() - menu.height(); + + if (x > xOff - 15) x = xOff - 15; + if (y > yOff - 30) y = yOff - 30; // 30 is needed to prevent scrollbars in FF + + // Show the menu + doc.unbind('click'); + menu.css({ top: y, left: x }).fadeIn(o.inSpeed); + // Hover events + menu.find('A').mouseover(function () { + menu.find('LI.hover').removeClass('hover'); + $(this).parent().addClass('hover'); + }).mouseout(function () { + menu.find('LI.hover').removeClass('hover'); + }); + + // Keyboard + doc.keypress(function (e) { + switch (e.keyCode) { + case 38: // up + if (!menu.find('LI.hover').length) { + menu.find('LI:last').addClass('hover'); + } else { + menu.find('LI.hover').removeClass('hover').prevAll('LI:not(.disabled)').eq(0).addClass('hover'); + if (!menu.find('LI.hover').length) menu.find('LI:last').addClass('hover'); + } + break; + case 40: // down + if (!menu.find('LI.hover').length) { + menu.find('LI:first').addClass('hover'); + } else { + menu.find('LI.hover').removeClass('hover').nextAll('LI:not(.disabled)').eq(0).addClass('hover'); + if (!menu.find('LI.hover').length) menu.find('LI:first').addClass('hover'); + } + break; + case 13: // enter + menu.find('LI.hover A').trigger('click'); + break; + case 27: // esc + doc.trigger('click'); + break; + } + }); + + // When items are selected + menu.find('A').unbind('mouseup'); + menu.find('LI:not(.disabled) A').mouseup(function () { + doc.unbind('click').unbind('keypress'); + $('.contextMenu').hide(); + // Callback + if (callback) { + callback($(this).attr('href').substr(1), $(srcElement), {x: x - offset.left, y: y - offset.top, docX: x, docY: y}); + } + return false; + }); + + // Hide bindings + setTimeout(function () { // Delay for Mozilla + doc.click(function () { + doc.unbind('click').unbind('keypress'); + menu.fadeOut(o.outSpeed); + return false; + }); + }, 0); + } + }); + }); + + // Disable text selection + if ($.browser.mozilla) { + $('#' + o.menu).each(function () { $(this).css({MozUserSelect: 'none'}); }); + } else if ($.browser.msie) { + $('#' + o.menu).each(function () { $(this).bind('selectstart.disableTextSelect', function () { return false; }); }); + } else { + $('#' + o.menu).each(function () { $(this).bind('mousedown.disableTextSelect', function () { return false; }); }); + } + // Disable browser context menu (requires both selectors to work in IE/Safari + FF/Chrome) + $(el).add($('UL.contextMenu')).bind('contextmenu', function () { return false; }); + }); + return $(this); + }, + + /** + * Disable context menu items on the fly + * @memberof external:jQuery.fn + * @param {undefined|string} o Comma-separated + * @returns {external:jQuery} + */ + disableContextMenuItems (o) { + if (o === undefined) { + // Disable all + $(this).find('LI').addClass('disabled'); + return $(this); + } + $(this).each(function () { + if (o !== undefined) { + const d = o.split(','); + for (let i = 0; i < d.length; i++) { + $(this).find('A[href="' + d[i] + '"]').parent().addClass('disabled'); + } + } + }); + return $(this); + }, + + /** + * Enable context menu items on the fly + * @memberof external:jQuery.fn + * @param {undefined|string} o Comma-separated + * @returns {external:jQuery} + */ + enableContextMenuItems (o) { + if (o === undefined) { + // Enable all + $(this).find('LI.disabled').removeClass('disabled'); + return $(this); + } + $(this).each(function () { + if (o !== undefined) { + const d = o.split(','); + for (let i = 0; i < d.length; i++) { + $(this).find('A[href="' + d[i] + '"]').parent().removeClass('disabled'); + } + } + }); + return $(this); + }, + + /** + * Disable context menu(s) + * @memberof external:jQuery.fn + * @returns {external:jQuery} + */ + disableContextMenu () { + $(this).each(function () { + $(this).addClass('disabled'); + }); + return $(this); + }, + + /** + * Enable context menu(s) + * @memberof external:jQuery.fn + * @returns {external:jQuery} + */ + enableContextMenu () { + $(this).each(function () { + $(this).removeClass('disabled'); + }); + return $(this); + }, + + /** + * Destroy context menu(s) + * @memberof external:jQuery.fn + * @returns {external:jQuery} + */ + destroyContextMenu () { + // Destroy specified context menus + $(this).each(function () { + // Disable action + $(this).unbind('mousedown').unbind('mouseup'); + }); + return $(this); + } + }); + return $; +} + +export default jQueryContextMenu; diff --git a/editor/contextmenu/jquery.contextMenu.js b/editor/contextmenu/jquery.contextMenu.js deleted file mode 100755 index 76126019..00000000 --- a/editor/contextmenu/jquery.contextMenu.js +++ /dev/null @@ -1,203 +0,0 @@ -// jQuery Context Menu Plugin -// -// Version 1.01 -// -// Cory S.N. LaViska -// A Beautiful Site (http://abeautifulsite.net/) -// Modified by Alexis Deveria -// -// More info: http://abeautifulsite.net/2008/09/jquery-context-menu-plugin/ -// -// Terms of Use -// -// This plugin is dual-licensed under the GNU General Public License -// and the MIT License and is copyright A Beautiful Site, LLC. -// -if(jQuery)( function() { - var win = $(window); - var doc = $(document); - - $.extend($.fn, { - - contextMenu: function(o, callback) { - // Defaults - if( o.menu == undefined ) return false; - if( o.inSpeed == undefined ) o.inSpeed = 150; - if( o.outSpeed == undefined ) o.outSpeed = 75; - // 0 needs to be -1 for expected results (no fade) - if( o.inSpeed == 0 ) o.inSpeed = -1; - if( o.outSpeed == 0 ) o.outSpeed = -1; - // Loop each context menu - $(this).each( function() { - var el = $(this); - var offset = $(el).offset(); - - var menu = $('#' + o.menu); - - // Add contextMenu class - menu.addClass('contextMenu'); - // Simulate a true right click - $(this).bind( "mousedown", function(e) { - var evt = e; - $(this).mouseup( function(e) { - var srcElement = $(this); - srcElement.unbind('mouseup'); - if( evt.button === 2 || o.allowLeft || (evt.ctrlKey && svgedit.browser.isMac()) ) { - e.stopPropagation(); - // Hide context menus that may be showing - $(".contextMenu").hide(); - // Get this context menu - - if( el.hasClass('disabled') ) return false; - - // Detect mouse position - var d = {}, x = e.pageX, y = e.pageY; - - var x_off = win.width() - menu.width(), - y_off = win.height() - menu.height(); - - if(x > x_off - 15) x = x_off-15; - if(y > y_off - 30) y = y_off-30; // 30 is needed to prevent scrollbars in FF - - // Show the menu - doc.unbind('click'); - menu.css({ top: y, left: x }).fadeIn(o.inSpeed); - // Hover events - menu.find('A').mouseover( function() { - menu.find('LI.hover').removeClass('hover'); - $(this).parent().addClass('hover'); - }).mouseout( function() { - menu.find('LI.hover').removeClass('hover'); - }); - - // Keyboard - doc.keypress( function(e) { - switch( e.keyCode ) { - case 38: // up - if( !menu.find('LI.hover').length ) { - menu.find('LI:last').addClass('hover'); - } else { - menu.find('LI.hover').removeClass('hover').prevAll('LI:not(.disabled)').eq(0).addClass('hover'); - if( !menu.find('LI.hover').length ) menu.find('LI:last').addClass('hover'); - } - break; - case 40: // down - if( menu.find('LI.hover').length == 0 ) { - menu.find('LI:first').addClass('hover'); - } else { - menu.find('LI.hover').removeClass('hover').nextAll('LI:not(.disabled)').eq(0).addClass('hover'); - if( !menu.find('LI.hover').length ) menu.find('LI:first').addClass('hover'); - } - break; - case 13: // enter - menu.find('LI.hover A').trigger('click'); - break; - case 27: // esc - doc.trigger('click'); - break - } - }); - - // When items are selected - menu.find('A').unbind('mouseup'); - menu.find('LI:not(.disabled) A').mouseup( function() { - doc.unbind('click').unbind('keypress'); - $(".contextMenu").hide(); - // Callback - if( callback ) callback( $(this).attr('href').substr(1), $(srcElement), {x: x - offset.left, y: y - offset.top, docX: x, docY: y} ); - return false; - }); - - // Hide bindings - setTimeout( function() { // Delay for Mozilla - doc.click( function() { - doc.unbind('click').unbind('keypress'); - menu.fadeOut(o.outSpeed); - return false; - }); - }, 0); - } - }); - }); - - // Disable text selection - if( $.browser.mozilla ) { - $('#' + o.menu).each( function() { $(this).css({ 'MozUserSelect' : 'none' }); }); - } else if( $.browser.msie ) { - $('#' + o.menu).each( function() { $(this).bind('selectstart.disableTextSelect', function() { return false; }); }); - } else { - $('#' + o.menu).each(function() { $(this).bind('mousedown.disableTextSelect', function() { return false; }); }); - } - // Disable browser context menu (requires both selectors to work in IE/Safari + FF/Chrome) - $(el).add($('UL.contextMenu')).bind('contextmenu', function() { return false; }); - - }); - return $(this); - }, - - // Disable context menu items on the fly - disableContextMenuItems: function(o) { - if( o == undefined ) { - // Disable all - $(this).find('LI').addClass('disabled'); - return( $(this) ); - } - $(this).each( function() { - if( o != undefined ) { - var d = o.split(','); - for( var i = 0; i < d.length; i++ ) { - $(this).find('A[href="' + d[i] + '"]').parent().addClass('disabled'); - - } - } - }); - return( $(this) ); - }, - - // Enable context menu items on the fly - enableContextMenuItems: function(o) { - if( o == undefined ) { - // Enable all - $(this).find('LI.disabled').removeClass('disabled'); - return( $(this) ); - } - $(this).each( function() { - if( o != undefined ) { - var d = o.split(','); - for( var i = 0; i < d.length; i++ ) { - $(this).find('A[href="' + d[i] + '"]').parent().removeClass('disabled'); - - } - } - }); - return( $(this) ); - }, - - // Disable context menu(s) - disableContextMenu: function() { - $(this).each( function() { - $(this).addClass('disabled'); - }); - return( $(this) ); - }, - - // Enable context menu(s) - enableContextMenu: function() { - $(this).each( function() { - $(this).removeClass('disabled'); - }); - return( $(this) ); - }, - - // Destroy context menu(s) - destroyContextMenu: function() { - // Destroy specified context menus - $(this).each( function() { - // Disable action - $(this).unbind('mousedown').unbind('mouseup'); - }); - return( $(this) ); - } - - }); -})(jQuery); \ No newline at end of file diff --git a/editor/coords.js b/editor/coords.js index 7495ee36..08758aba 100644 --- a/editor/coords.js +++ b/editor/coords.js @@ -1,317 +1,313 @@ -/*globals $, svgroot */ -/*jslint vars: true, eqeq: true, forin: true*/ +/* globals jQuery */ /** - * Coords. - * - * Licensed under the MIT License - * + * Manipulating coordinates + * @module coords + * @license MIT */ -// Dependencies: -// 1) jquery.js -// 2) math.js -// 3) pathseg.js -// 4) browser.js -// 5) svgutils.js -// 6) units.js -// 7) svgtransformlist.js +import './svgpathseg.js'; +import { + snapToGrid, assignAttributes, getBBox, getRefElem, findDefs +} from './utilities.js'; +import { + transformPoint, transformListToTransform, matrixMultiply, transformBox +} from './math.js'; +import {getTransformList} from './svgtransformlist.js'; -var svgedit = svgedit || {}; - -(function() {'use strict'; - -if (!svgedit.coords) { - svgedit.coords = {}; -} +const $ = jQuery; // this is how we map paths to our preferred relative segment types -var pathMap = [0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a', - 'H', 'h', 'V', 'v', 'S', 's', 'T', 't']; +const pathMap = [0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a', + 'H', 'h', 'V', 'v', 'S', 's', 'T', 't']; /** - * @typedef editorContext - * @type {?object} - * @property {function} getGridSnapping - * @property {function} getDrawing + * @interface module:coords.EditorContext + */ +/** + * @function module:coords.EditorContext#getGridSnapping + * @returns {boolean} + */ +/** + * @function module:coords.EditorContext#getDrawing + * @returns {module:draw.Drawing} */ -var editorContext_ = null; +/** + * @function module:coords.EditorContext#getSVGRoot + * @returns {SVGSVGElement} +*/ + +let editorContext_ = null; /** -* @param {editorContext} editorContext +* @function module:coords.init +* @param {module:coords.EditorContext} editorContext */ -svgedit.coords.init = function(editorContext) { +export const init = function (editorContext) { editorContext_ = editorContext; }; /** * Applies coordinate changes to an element based on the given matrix - * @param {Element} selected - DOM element to be changed - * @param {object} changes - Object with changes to be remapped - * @param {SVGMatrix} m - Matrix object to use for remapping coordinates + * @function module:coords.remapElement + * @implements {module:path.EditorContext#remapElement} */ -svgedit.coords.remapElement = function(selected, changes, m) { - var i, type, - remap = function(x, y) { return svgedit.math.transformPoint(x, y, m); }, - scalew = function(w) { return m.a * w; }, - scaleh = function(h) { return m.d * h; }, +export const remapElement = function (selected, changes, m) { + const remap = function (x, y) { return transformPoint(x, y, m); }, + scalew = function (w) { return m.a * w; }, + scaleh = function (h) { return m.d * h; }, doSnapping = editorContext_.getGridSnapping() && selected.parentNode.parentNode.localName === 'svg', - finishUp = function() { - var o; + finishUp = function () { if (doSnapping) { - for (o in changes) { - changes[o] = svgedit.utilities.snapToGrid(changes[o]); + for (const o in changes) { + changes[o] = snapToGrid(changes[o]); } } - svgedit.utilities.assignAttributes(selected, changes, 1000, true); + assignAttributes(selected, changes, 1000, true); }, - box = svgedit.utilities.getBBox(selected); + box = getBBox(selected); - for (i = 0; i < 2; i++) { - type = i === 0 ? 'fill' : 'stroke'; - var attrVal = selected.getAttribute(type); - if (attrVal && attrVal.indexOf('url(') === 0) { + for (let i = 0; i < 2; i++) { + const type = i === 0 ? 'fill' : 'stroke'; + const attrVal = selected.getAttribute(type); + if (attrVal && attrVal.startsWith('url(')) { if (m.a < 0 || m.d < 0) { - var grad = svgedit.utilities.getRefElem(attrVal); - var newgrad = grad.cloneNode(true); + const grad = getRefElem(attrVal); + const newgrad = grad.cloneNode(true); if (m.a < 0) { // flip x - var x1 = newgrad.getAttribute('x1'); - var x2 = newgrad.getAttribute('x2'); + const x1 = newgrad.getAttribute('x1'); + const x2 = newgrad.getAttribute('x2'); newgrad.setAttribute('x1', -(x1 - 1)); newgrad.setAttribute('x2', -(x2 - 1)); - } + } if (m.d < 0) { // flip y - var y1 = newgrad.getAttribute('y1'); - var y2 = newgrad.getAttribute('y2'); + const y1 = newgrad.getAttribute('y1'); + const y2 = newgrad.getAttribute('y2'); newgrad.setAttribute('y1', -(y1 - 1)); newgrad.setAttribute('y2', -(y2 - 1)); } newgrad.id = editorContext_.getDrawing().getNextId(); - svgedit.utilities.findDefs().appendChild(newgrad); + findDefs().append(newgrad); selected.setAttribute(type, 'url(#' + newgrad.id + ')'); } // Not really working :( -// if (selected.tagName === 'path') { -// reorientGrads(selected, m); -// } + // if (selected.tagName === 'path') { + // reorientGrads(selected, m); + // } } } - var elName = selected.tagName; - var chlist, mt; - if (elName === 'g' || elName === 'text' || elName == 'tspan' || elName === 'use') { + const elName = selected.tagName; + if (elName === 'g' || elName === 'text' || elName === 'tspan' || elName === 'use') { // if it was a translate, then just update x,y - if (m.a == 1 && m.b == 0 && m.c == 0 && m.d == 1 && (m.e != 0 || m.f != 0) ) { + if (m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && (m.e !== 0 || m.f !== 0)) { // [T][M] = [M][T'] // therefore [T'] = [M_inv][T][M] - var existing = svgedit.math.transformListToTransform(selected).matrix, - t_new = svgedit.math.matrixMultiply(existing.inverse(), m, existing); - changes.x = parseFloat(changes.x) + t_new.e; - changes.y = parseFloat(changes.y) + t_new.f; + const existing = transformListToTransform(selected).matrix, + tNew = matrixMultiply(existing.inverse(), m, existing); + changes.x = parseFloat(changes.x) + tNew.e; + changes.y = parseFloat(changes.y) + tNew.f; } else { // we just absorb all matrices into the element and don't do any remapping - chlist = svgedit.transformlist.getTransformList(selected); - mt = svgroot.createSVGTransform(); - mt.setMatrix(svgedit.math.matrixMultiply(svgedit.math.transformListToTransform(chlist).matrix, m)); + const chlist = getTransformList(selected); + const mt = editorContext_.getSVGRoot().createSVGTransform(); + mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix, m)); chlist.clear(); chlist.appendItem(mt); } } - var c, pt, pt1, pt2, len; + // now we have a set of changes and an applied reduced transform list // we apply the changes directly to the DOM switch (elName) { - case 'foreignObject': - case 'rect': - case 'image': - // Allow images to be inverted (give them matrix when flipped) - if (elName === 'image' && (m.a < 0 || m.d < 0)) { - // Convert to matrix - chlist = svgedit.transformlist.getTransformList(selected); - mt = svgroot.createSVGTransform(); - mt.setMatrix(svgedit.math.matrixMultiply(svgedit.math.transformListToTransform(chlist).matrix, m)); - chlist.clear(); - chlist.appendItem(mt); - } else { - pt1 = remap(changes.x, changes.y); - changes.width = scalew(changes.width); - changes.height = scaleh(changes.height); - changes.x = pt1.x + Math.min(0, changes.width); - changes.y = pt1.y + Math.min(0, changes.height); - changes.width = Math.abs(changes.width); - changes.height = Math.abs(changes.height); - } - finishUp(); - break; - case 'ellipse': - c = remap(changes.cx, changes.cy); - changes.cx = c.x; - changes.cy = c.y; - changes.rx = scalew(changes.rx); - changes.ry = scaleh(changes.ry); - changes.rx = Math.abs(changes.rx); - changes.ry = Math.abs(changes.ry); - finishUp(); - break; - case 'circle': - c = remap(changes.cx,changes.cy); - changes.cx = c.x; - changes.cy = c.y; - // take the minimum of the new selected box's dimensions for the new circle radius - var tbox = svgedit.math.transformBox(box.x, box.y, box.width, box.height, m); - var w = tbox.tr.x - tbox.tl.x, h = tbox.bl.y - tbox.tl.y; - changes.r = Math.min(w/2, h/2); - - if (changes.r) {changes.r = Math.abs(changes.r);} - finishUp(); - break; - case 'line': - pt1 = remap(changes.x1, changes.y1); - pt2 = remap(changes.x2, changes.y2); - changes.x1 = pt1.x; - changes.y1 = pt1.y; - changes.x2 = pt2.x; - changes.y2 = pt2.y; - // deliberately fall through here - case 'text': - case 'tspan': - case 'use': - finishUp(); - break; - case 'g': - var gsvg = $(selected).data('gsvg'); - if (gsvg) { - svgedit.utilities.assignAttributes(gsvg, changes, 1000, true); - } - break; - case 'polyline': - case 'polygon': - len = changes.points.length; - for (i = 0; i < len; ++i) { - pt = changes.points[i]; - pt = remap(pt.x, pt.y); - changes.points[i].x = pt.x; - changes.points[i].y = pt.y; - } - - len = changes.points.length; - var pstr = ''; - for (i = 0; i < len; ++i) { - pt = changes.points[i]; - pstr += pt.x + ',' + pt.y + ' '; - } - selected.setAttribute('points', pstr); - break; - case 'path': - var seg; - var segList = selected.pathSegList; - len = segList.numberOfItems; - changes.d = []; - for (i = 0; i < len; ++i) { - seg = segList.getItem(i); - changes.d[i] = { - type: seg.pathSegType, - x: seg.x, - y: seg.y, - x1: seg.x1, - y1: seg.y1, - x2: seg.x2, - y2: seg.y2, - r1: seg.r1, - r2: seg.r2, - angle: seg.angle, - largeArcFlag: seg.largeArcFlag, - sweepFlag: seg.sweepFlag - }; - } - - len = changes.d.length; - var firstseg = changes.d[0], - currentpt = remap(firstseg.x, firstseg.y); - changes.d[0].x = currentpt.x; - changes.d[0].y = currentpt.y; - for (i = 1; i < len; ++i) { - seg = changes.d[i]; - type = seg.type; - // if absolute or first segment, we want to remap x, y, x1, y1, x2, y2 - // if relative, we want to scalew, scaleh - if (type % 2 == 0) { // absolute - var thisx = (seg.x != undefined) ? seg.x : currentpt.x, // for V commands - thisy = (seg.y != undefined) ? seg.y : currentpt.y; // for H commands - pt = remap(thisx,thisy); - pt1 = remap(seg.x1, seg.y1); - pt2 = remap(seg.x2, seg.y2); - seg.x = pt.x; - seg.y = pt.y; - seg.x1 = pt1.x; - seg.y1 = pt1.y; - seg.x2 = pt2.x; - seg.y2 = pt2.y; - seg.r1 = scalew(seg.r1); - seg.r2 = scaleh(seg.r2); - } - else { // relative - seg.x = scalew(seg.x); - seg.y = scaleh(seg.y); - seg.x1 = scalew(seg.x1); - seg.y1 = scaleh(seg.y1); - seg.x2 = scalew(seg.x2); - seg.y2 = scaleh(seg.y2); - seg.r1 = scalew(seg.r1); - seg.r2 = scaleh(seg.r2); - } - } // for each segment - - var dstr = ''; - len = changes.d.length; - for (i = 0; i < len; ++i) { - seg = changes.d[i]; - type = seg.type; - dstr += pathMap[type]; - switch (type) { - case 13: // relative horizontal line (h) - case 12: // absolute horizontal line (H) - dstr += seg.x + ' '; - break; - case 15: // relative vertical line (v) - case 14: // absolute vertical line (V) - dstr += seg.y + ' '; - break; - case 3: // relative move (m) - case 5: // relative line (l) - case 19: // relative smooth quad (t) - case 2: // absolute move (M) - case 4: // absolute line (L) - case 18: // absolute smooth quad (T) - dstr += seg.x + ',' + seg.y + ' '; - break; - case 7: // relative cubic (c) - case 6: // absolute cubic (C) - dstr += seg.x1 + ',' + seg.y1 + ' ' + seg.x2 + ',' + seg.y2 + ' ' + - seg.x + ',' + seg.y + ' '; - break; - case 9: // relative quad (q) - case 8: // absolute quad (Q) - dstr += seg.x1 + ',' + seg.y1 + ' ' + seg.x + ',' + seg.y + ' '; - break; - case 11: // relative elliptical arc (a) - case 10: // absolute elliptical arc (A) - dstr += seg.r1 + ',' + seg.r2 + ' ' + seg.angle + ' ' + (+seg.largeArcFlag) + - ' ' + (+seg.sweepFlag) + ' ' + seg.x + ',' + seg.y + ' '; - break; - case 17: // relative smooth cubic (s) - case 16: // absolute smooth cubic (S) - dstr += seg.x2 + ',' + seg.y2 + ' ' + seg.x + ',' + seg.y + ' '; - break; - } - } - - selected.setAttribute('d', dstr); - break; + case 'foreignObject': + case 'rect': + case 'image': { + // Allow images to be inverted (give them matrix when flipped) + if (elName === 'image' && (m.a < 0 || m.d < 0)) { + // Convert to matrix + const chlist = getTransformList(selected); + const mt = editorContext_.getSVGRoot().createSVGTransform(); + mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix, m)); + chlist.clear(); + chlist.appendItem(mt); + } else { + const pt1 = remap(changes.x, changes.y); + changes.width = scalew(changes.width); + changes.height = scaleh(changes.height); + changes.x = pt1.x + Math.min(0, changes.width); + changes.y = pt1.y + Math.min(0, changes.height); + changes.width = Math.abs(changes.width); + changes.height = Math.abs(changes.height); } -}; + finishUp(); + break; + } case 'ellipse': { + const c = remap(changes.cx, changes.cy); + changes.cx = c.x; + changes.cy = c.y; + changes.rx = scalew(changes.rx); + changes.ry = scaleh(changes.ry); + changes.rx = Math.abs(changes.rx); + changes.ry = Math.abs(changes.ry); + finishUp(); + break; + } case 'circle': { + const c = remap(changes.cx, changes.cy); + changes.cx = c.x; + changes.cy = c.y; + // take the minimum of the new selected box's dimensions for the new circle radius + const tbox = transformBox(box.x, box.y, box.width, box.height, m); + const w = tbox.tr.x - tbox.tl.x, h = tbox.bl.y - tbox.tl.y; + changes.r = Math.min(w / 2, h / 2); -}()); + if (changes.r) { changes.r = Math.abs(changes.r); } + finishUp(); + break; + } case 'line': { + const pt1 = remap(changes.x1, changes.y1); + const pt2 = remap(changes.x2, changes.y2); + changes.x1 = pt1.x; + changes.y1 = pt1.y; + changes.x2 = pt2.x; + changes.y2 = pt2.y; + } // Fallthrough + case 'text': + case 'tspan': + case 'use': { + finishUp(); + break; + } case 'g': { + const gsvg = $(selected).data('gsvg'); + if (gsvg) { + assignAttributes(gsvg, changes, 1000, true); + } + break; + } case 'polyline': + case 'polygon': { + const len = changes.points.length; + for (let i = 0; i < len; ++i) { + const pt = changes.points[i]; + const {x, y} = remap(pt.x, pt.y); + changes.points[i].x = x; + changes.points[i].y = y; + } + + // const len = changes.points.length; + let pstr = ''; + for (let i = 0; i < len; ++i) { + const pt = changes.points[i]; + pstr += pt.x + ',' + pt.y + ' '; + } + selected.setAttribute('points', pstr); + break; + } case 'path': { + const segList = selected.pathSegList; + let len = segList.numberOfItems; + changes.d = []; + for (let i = 0; i < len; ++i) { + const seg = segList.getItem(i); + changes.d[i] = { + type: seg.pathSegType, + x: seg.x, + y: seg.y, + x1: seg.x1, + y1: seg.y1, + x2: seg.x2, + y2: seg.y2, + r1: seg.r1, + r2: seg.r2, + angle: seg.angle, + largeArcFlag: seg.largeArcFlag, + sweepFlag: seg.sweepFlag + }; + } + + len = changes.d.length; + const firstseg = changes.d[0], + currentpt = remap(firstseg.x, firstseg.y); + changes.d[0].x = currentpt.x; + changes.d[0].y = currentpt.y; + for (let i = 1; i < len; ++i) { + const seg = changes.d[i]; + const {type} = seg; + // if absolute or first segment, we want to remap x, y, x1, y1, x2, y2 + // if relative, we want to scalew, scaleh + if (type % 2 === 0) { // absolute + const thisx = (seg.x !== undefined) ? seg.x : currentpt.x, // for V commands + thisy = (seg.y !== undefined) ? seg.y : currentpt.y; // for H commands + const pt = remap(thisx, thisy); + const pt1 = remap(seg.x1, seg.y1); + const pt2 = remap(seg.x2, seg.y2); + seg.x = pt.x; + seg.y = pt.y; + seg.x1 = pt1.x; + seg.y1 = pt1.y; + seg.x2 = pt2.x; + seg.y2 = pt2.y; + seg.r1 = scalew(seg.r1); + seg.r2 = scaleh(seg.r2); + } else { // relative + seg.x = scalew(seg.x); + seg.y = scaleh(seg.y); + seg.x1 = scalew(seg.x1); + seg.y1 = scaleh(seg.y1); + seg.x2 = scalew(seg.x2); + seg.y2 = scaleh(seg.y2); + seg.r1 = scalew(seg.r1); + seg.r2 = scaleh(seg.r2); + } + } // for each segment + + let dstr = ''; + len = changes.d.length; + for (let i = 0; i < len; ++i) { + const seg = changes.d[i]; + const {type} = seg; + dstr += pathMap[type]; + switch (type) { + case 13: // relative horizontal line (h) + case 12: // absolute horizontal line (H) + dstr += seg.x + ' '; + break; + case 15: // relative vertical line (v) + case 14: // absolute vertical line (V) + dstr += seg.y + ' '; + break; + case 3: // relative move (m) + case 5: // relative line (l) + case 19: // relative smooth quad (t) + case 2: // absolute move (M) + case 4: // absolute line (L) + case 18: // absolute smooth quad (T) + dstr += seg.x + ',' + seg.y + ' '; + break; + case 7: // relative cubic (c) + case 6: // absolute cubic (C) + dstr += seg.x1 + ',' + seg.y1 + ' ' + seg.x2 + ',' + seg.y2 + ' ' + + seg.x + ',' + seg.y + ' '; + break; + case 9: // relative quad (q) + case 8: // absolute quad (Q) + dstr += seg.x1 + ',' + seg.y1 + ' ' + seg.x + ',' + seg.y + ' '; + break; + case 11: // relative elliptical arc (a) + case 10: // absolute elliptical arc (A) + dstr += seg.r1 + ',' + seg.r2 + ' ' + seg.angle + ' ' + (+seg.largeArcFlag) + + ' ' + (+seg.sweepFlag) + ' ' + seg.x + ',' + seg.y + ' '; + break; + case 17: // relative smooth cubic (s) + case 16: // absolute smooth cubic (S) + dstr += seg.x2 + ',' + seg.y2 + ' ' + seg.x + ',' + seg.y + ' '; + break; + } + } + + selected.setAttribute('d', dstr); + break; + } + } +}; diff --git a/editor/draw.js b/editor/draw.js index c21c394f..0e70b5c0 100644 --- a/editor/draw.js +++ b/editor/draw.js @@ -1,671 +1,1043 @@ -/*globals $, svgedit*/ -/*jslint vars: true, eqeq: true, todo: true*/ +/* globals jQuery */ /** - * Package: svgedit.draw - * - * Licensed under the MIT License - * - * Copyright(c) 2011 Jeff Schiller + * Tools for drawing + * @module draw + * @license MIT + * @copyright 2011 Jeff Schiller */ -// Dependencies: -// 1) jQuery -// 2) browser.js -// 3) svgutils.js +import Layer from './layer.js'; +import HistoryRecordingService from './historyrecording.js'; -(function() {'use strict'; +import {NS} from './namespaces.js'; +import {isOpera} from './browser.js'; +import { + toXml, getElem, + copyElem as utilCopyElem +} from './utilities.js'; +import { + BatchCommand, RemoveElementCommand, MoveElementCommand, ChangeElementCommand +} from './history.js'; -if (!svgedit.draw) { - svgedit.draw = {}; +const $ = jQuery; + +const visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use'.split(','); + +const RandomizeModes = { + LET_DOCUMENT_DECIDE: 0, + ALWAYS_RANDOMIZE: 1, + NEVER_RANDOMIZE: 2 +}; +let randIds = RandomizeModes.LET_DOCUMENT_DECIDE; +// Array with current disabled elements (for in-group editing) +let disabledElems = []; + +/** + * Get a HistoryRecordingService. + * @param {module:history.HistoryRecordingService} [hrService] - if exists, return it instead of creating a new service. + * @returns {module:history.HistoryRecordingService} + */ +function historyRecordingService (hrService) { + return hrService || new HistoryRecordingService(canvas_.undoMgr); } -// alias -var NS = svgedit.NS; - -var visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use'.split(','); - -var RandomizeModes = { - LET_DOCUMENT_DECIDE: 0, - ALWAYS_RANDOMIZE: 1, - NEVER_RANDOMIZE: 2 -}; -var randomize_ids = RandomizeModes.LET_DOCUMENT_DECIDE; - - - - - /** - * Called to ensure that drawings will or will not have randomized ids. - * The currentDrawing will have its nonce set if it doesn't already. - * @param {boolean} enableRandomization - flag indicating if documents should have randomized ids - * @param {svgedit.draw.Drawing} currentDrawing - */ -svgedit.draw.randomizeIds = function(enableRandomization, currentDrawing) { - randomize_ids = enableRandomization === false ? - RandomizeModes.NEVER_RANDOMIZE : - RandomizeModes.ALWAYS_RANDOMIZE; - - if (randomize_ids == RandomizeModes.ALWAYS_RANDOMIZE && !currentDrawing.getNonce()) { - currentDrawing.setNonce(Math.floor(Math.random() * 100001)); - } else if (randomize_ids == RandomizeModes.NEVER_RANDOMIZE && currentDrawing.getNonce()) { - currentDrawing.clearNonce(); - } -}; - -/** - * This class encapsulates the concept of a SVG-edit drawing - * @param {SVGSVGElement} svgElem - The SVG DOM Element that this JS object - * encapsulates. If the svgElem has a se:nonce attribute on it, then - * IDs will use the nonce as they are generated. - * @param {String=svg_} [opt_idPrefix] - The ID prefix to use. - */ -svgedit.draw.Drawing = function(svgElem, opt_idPrefix) { - if (!svgElem || !svgElem.tagName || !svgElem.namespaceURI || - svgElem.tagName != 'svg' || svgElem.namespaceURI != NS.SVG) { - throw "Error: svgedit.draw.Drawing instance initialized without a element"; - } - - /** - * The SVG DOM Element that represents this drawing. - * @type {SVGSVGElement} - */ - this.svgElem_ = svgElem; - - /** - * The latest object number used in this drawing. - * @type {number} - */ - this.obj_num = 0; - - /** - * The prefix to prepend to each element id in the drawing. - * @type {String} - */ - this.idPrefix = opt_idPrefix || "svg_"; - - /** - * An array of released element ids to immediately reuse. - * @type {Array.} - */ - this.releasedNums = []; - - /** - * The z-ordered array of Layer objects. Each layer has a name - * and group element. - * The first layer is the one at the bottom of the rendering. - * @type {Array.} - */ - this.all_layers = []; - - /** - * Map of all_layers by name. - * - * Note: Layers are ordered, but referenced externally by name; so, we need both container - * types depending on which function is called (i.e. all_layers and layer_map). - * - * @type {Object.} - */ - this.layer_map = {}; - - /** - * The current layer being used. - * @type {Layer} - */ - this.current_layer = null; - - /** - * The nonce to use to uniquely identify elements across drawings. - * @type {!String} - */ - this.nonce_ = ''; - var n = this.svgElem_.getAttributeNS(NS.SE, 'nonce'); - // If already set in the DOM, use the nonce throughout the document - // else, if randomizeIds(true) has been called, create and set the nonce. - if (!!n && randomize_ids != RandomizeModes.NEVER_RANDOMIZE) { - this.nonce_ = n; - } else if (randomize_ids == RandomizeModes.ALWAYS_RANDOMIZE) { - this.setNonce(Math.floor(Math.random() * 100001)); - } -}; - -/** - * @param {string} id Element ID to retrieve - * @returns {Element} SVG element within the root SVGSVGElement -*/ -svgedit.draw.Drawing.prototype.getElem_ = function (id) { - if (this.svgElem_.querySelector) { - // querySelector lookup - return this.svgElem_.querySelector('#' + id); - } - // jQuery lookup: twice as slow as xpath in FF - return $(this.svgElem_).find('[id=' + id + ']')[0]; -}; - -/** - * @returns {SVGSVGElement} - */ -svgedit.draw.Drawing.prototype.getSvgElem = function () { - return this.svgElem_; -}; - -/** - * @returns {!string|number} The previously set nonce - */ -svgedit.draw.Drawing.prototype.getNonce = function() { - return this.nonce_; -}; - -/** - * @param {!string|number} n The nonce to set - */ -svgedit.draw.Drawing.prototype.setNonce = function(n) { - this.svgElem_.setAttributeNS(NS.XMLNS, 'xmlns:se', NS.SE); - this.svgElem_.setAttributeNS(NS.SE, 'se:nonce', n); - this.nonce_ = n; -}; - -/** - * Clears any previously set nonce - */ -svgedit.draw.Drawing.prototype.clearNonce = function () { - // We deliberately leave any se:nonce attributes alone, - // we just don't use it to randomize ids. - this.nonce_ = ''; -}; - -/** - * Returns the latest object id as a string. - * @return {String} The latest object Id. - */ -svgedit.draw.Drawing.prototype.getId = function () { - return this.nonce_ ? - this.idPrefix + this.nonce_ + '_' + this.obj_num : - this.idPrefix + this.obj_num; -}; - -/** - * Returns the next object Id as a string. - * @return {String} The next object Id to use. - */ -svgedit.draw.Drawing.prototype.getNextId = function () { - var oldObjNum = this.obj_num; - var restoreOldObjNum = false; - - // If there are any released numbers in the release stack, - // use the last one instead of the next obj_num. - // We need to temporarily use obj_num as that is what getId() depends on. - if (this.releasedNums.length > 0) { - this.obj_num = this.releasedNums.pop(); - restoreOldObjNum = true; - } else { - // If we are not using a released id, then increment the obj_num. - this.obj_num++; - } - - // Ensure the ID does not exist. - var id = this.getId(); - while (this.getElem_(id)) { - if (restoreOldObjNum) { - this.obj_num = oldObjNum; - restoreOldObjNum = false; - } - this.obj_num++; - id = this.getId(); - } - // Restore the old object number if required. - if (restoreOldObjNum) { - this.obj_num = oldObjNum; - } - return id; -}; - -/** - * Releases the object Id, letting it be used as the next id in getNextId(). - * This method DOES NOT remove any elements from the DOM, it is expected - * that client code will do this. - * @param {string} id - The id to release. - * @returns {boolean} True if the id was valid to be released, false otherwise. -*/ -svgedit.draw.Drawing.prototype.releaseId = function (id) { - // confirm if this is a valid id for this Document, else return false - var front = this.idPrefix + (this.nonce_ ? this.nonce_ + '_' : ''); - if (typeof id !== 'string' || id.indexOf(front) !== 0) { - return false; - } - // extract the obj_num of this id - var num = parseInt(id.substr(front.length), 10); - - // if we didn't get a positive number or we already released this number - // then return false. - if (typeof num !== 'number' || num <= 0 || this.releasedNums.indexOf(num) != -1) { - return false; - } - - // push the released number into the released queue - this.releasedNums.push(num); - - return true; -}; - -/** - * Returns the number of layers in the current drawing. - * @returns {integer} The number of layers in the current drawing. -*/ -svgedit.draw.Drawing.prototype.getNumLayers = function() { - return this.all_layers.length; -}; - -/** - * Check if layer with given name already exists - * @param {string} name - The layer name to check -*/ -svgedit.draw.Drawing.prototype.hasLayer = function (name) { - return this.layer_map[name] !== undefined; -}; - - -/** - * Returns the name of the ith layer. If the index is out of range, an empty string is returned. - * @param {integer} i - The zero-based index of the layer you are querying. - * @returns {string} The name of the ith layer (or the empty string if none found) -*/ -svgedit.draw.Drawing.prototype.getLayerName = function (i) { - return i >= 0 && i < this.getNumLayers() ? this.all_layers[i].getName() : ''; -}; - -/** - * @returns {SVGGElement} The SVGGElement representing the current layer. - */ -svgedit.draw.Drawing.prototype.getCurrentLayer = function() { - return this.current_layer ? this.current_layer.getGroup() : null; -}; - -/** - * Get a layer by name. - * @returns {SVGGElement} The SVGGElement representing the named layer or null. - */ -svgedit.draw.Drawing.prototype.getLayerByName = function(name) { - var layer = this.layer_map[name]; - return layer ? layer.getGroup() : null; -}; - -/** - * Returns the name of the currently selected layer. If an error occurs, an empty string - * is returned. - * @returns {string} The name of the currently active layer (or the empty string if none found). -*/ -svgedit.draw.Drawing.prototype.getCurrentLayerName = function () { - return this.current_layer ? this.current_layer.getName() : ''; -}; - -/** - * Set the current layer's name. - * @param {string} name - The new name. - * @param {svgedit.history.HistoryRecordingService} hrService - History recording service - * @returns {string|null} The new name if changed; otherwise, null. - */ -svgedit.draw.Drawing.prototype.setCurrentLayerName = function (name, hrService) { - var finalName = null; - if (this.current_layer) { - var oldName = this.current_layer.getName(); - finalName = this.current_layer.setName(name, hrService); - if (finalName) { - delete this.layer_map[oldName]; - this.layer_map[finalName] = this.current_layer; - } - } - return finalName; -}; - -/** - * Set the current layer's position. - * @param {number} newpos - The zero-based index of the new position of the layer. Range should be 0 to layers-1 - * @returns {Object} If the name was changed, returns {title:SVGGElement, previousName:string}; otherwise null. - */ -svgedit.draw.Drawing.prototype.setCurrentLayerPosition = function (newpos) { - var layer_count = this.getNumLayers(); - if (!this.current_layer || newpos < 0 || newpos >= layer_count) { - return null; - } - - var oldpos; - for (oldpos = 0; oldpos < layer_count; ++oldpos) { - if (this.all_layers[oldpos] == this.current_layer) {break;} - } - // some unknown error condition (current_layer not in all_layers) - if (oldpos == layer_count) { return null; } - - if (oldpos != newpos) { - // if our new position is below us, we need to insert before the node after newpos - var refGroup = null; - var current_group = this.current_layer.getGroup(); - var oldNextSibling = current_group.nextSibling; - if (newpos > oldpos ) { - if (newpos < layer_count-1) { - refGroup = this.all_layers[newpos+1].getGroup(); - } - } - // if our new position is above us, we need to insert before the node at newpos - else { - refGroup = this.all_layers[newpos].getGroup(); - } - this.svgElem_.insertBefore(current_group, refGroup); - - this.identifyLayers(); - this.setCurrentLayer(this.getLayerName(newpos)); - - return { - currentGroup: current_group, - oldNextSibling: oldNextSibling - }; - } - return null; -}; - -svgedit.draw.Drawing.prototype.mergeLayer = function (hrService) { - var current_group = this.current_layer.getGroup(); - var prevGroup = $(current_group).prev()[0]; - if (!prevGroup) {return;} - - hrService.startBatchCommand('Merge Layer'); - - var layerNextSibling = current_group.nextSibling; - hrService.removeElement(current_group, layerNextSibling, this.svgElem_); - - while (current_group.firstChild) { - var child = current_group.firstChild; - if (child.localName == 'title') { - hrService.removeElement(child, child.nextSibling, current_group); - current_group.removeChild(child); - continue; - } - var oldNextSibling = child.nextSibling; - prevGroup.appendChild(child); - hrService.moveElement(child, oldNextSibling, current_group); - } - - // Remove current layer's group - this.current_layer.removeGroup(); - // Remove the current layer and set the previous layer as the new current layer - var index = this.all_layers.indexOf(this.current_layer); - if (index > 0) { - var name = this.current_layer.getName(); - this.current_layer = this.all_layers[index-1] - this.all_layers.splice(index, 1); - delete this.layer_map[name]; - } - - hrService.endBatchCommand(); -}; - -svgedit.draw.Drawing.prototype.mergeAllLayers = function (hrService) { - // Set the current layer to the last layer. - this.current_layer = this.all_layers[this.all_layers.length-1]; - - hrService.startBatchCommand('Merge all Layers'); - while (this.all_layers.length > 1) { - this.mergeLayer(hrService); - } - hrService.endBatchCommand(); -}; - -/** - * Sets the current layer. If the name is not a valid layer name, then this - * function returns false. Otherwise it returns true. This is not an - * undo-able action. - * @param {string} name - The name of the layer you want to switch to. - * @returns {boolean} true if the current layer was switched, otherwise false - */ -svgedit.draw.Drawing.prototype.setCurrentLayer = function(name) { - var layer = this.layer_map[name]; - if (layer) { - if (this.current_layer) { - this.current_layer.deactivate(); - } - this.current_layer = layer; - this.current_layer.activate(); - return true; - } - return false; -}; - - -/** - * Deletes the current layer from the drawing and then clears the selection. - * This function then calls the 'changed' handler. This is an undoable action. - * @returns {SVGGElement} The SVGGElement of the layer removed or null. - */ -svgedit.draw.Drawing.prototype.deleteCurrentLayer = function() { - if (this.current_layer && this.getNumLayers() > 1) { - var oldLayerGroup = this.current_layer.removeGroup(); - this.identifyLayers(); - return oldLayerGroup; - } - return null; -}; /** * Find the layer name in a group element. - * @param group The group element to search in. + * @param {Element} group The group element to search in. * @returns {string} The layer name or empty string. */ -function findLayerNameInGroup(group) { - var name = $("title", group).text(); - - // Hack for Opera 10.60 - if (!name && svgedit.browser.isOpera() && group.querySelectorAll) { - name = $(group.querySelectorAll('title')).text(); - } - return name; +function findLayerNameInGroup (group) { + return $('title', group).text() || + (isOpera() && group.querySelectorAll + // Hack for Opera 10.60 + ? $(group.querySelectorAll('title')).text() + : ''); } /** * Given a set of names, return a new unique name. - * @param {Array.} existingLayerNames - Existing layer names. + * @param {string[]} existingLayerNames - Existing layer names. * @returns {string} - The new name. */ -function getNewLayerName(existingLayerNames) { - var i = 1; - // TODO(codedread): What about internationalization of "Layer"? - while (existingLayerNames.indexOf(("Layer " + i)) >= 0) { i++; } - return "Layer " + i; +function getNewLayerName (existingLayerNames) { + let i = 1; + // TODO(codedread): What about internationalization of "Layer"? + while (existingLayerNames.includes(('Layer ' + i))) { i++; } + return 'Layer ' + i; } /** - * Updates layer system and sets the current layer to the - * top-most layer (last child of this drawing). -*/ -svgedit.draw.Drawing.prototype.identifyLayers = function() { - this.all_layers = []; - this.layer_map = {}; - var numchildren = this.svgElem_.childNodes.length; - // loop through all children of SVG element - var orphans = [], layernames = []; - var layer = null; - var childgroups = false; - for (var i = 0; i < numchildren; ++i) { - var child = this.svgElem_.childNodes.item(i); - // for each g, find its layer name - if (child && child.nodeType == 1) { - if (child.tagName == "g") { - childgroups = true; - var name = findLayerNameInGroup(child); - if (name) { - layernames.push(name); - layer = new svgedit.draw.Layer(name, child); - this.all_layers.push(layer); - this.layer_map[name] = layer; - } else { - // if group did not have a name, it is an orphan - orphans.push(child); - } - } else if (~visElems.indexOf(child.nodeName)) { - // Child is "visible" (i.e. not a or element), so it is an orphan - orphans.push(child); - } - } - } - - // If orphans or no layers found, create a new layer and add all the orphans to it - if (orphans.length > 0 || !childgroups) { - layer = new svgedit.draw.Layer(getNewLayerName(layernames), null, this.svgElem_); - layer.appendChildren(orphans); - this.all_layers.push(layer); - this.layer_map[name] = layer; - } else { - layer.activate(); - } - this.current_layer = layer; -}; + * This class encapsulates the concept of a SVG-edit drawing + */ +export class Drawing { + /** + * @param {SVGSVGElement} svgElem - The SVG DOM Element that this JS object + * encapsulates. If the svgElem has a se:nonce attribute on it, then + * IDs will use the nonce as they are generated. + * @param {string} [optIdPrefix=svg_] - The ID prefix to use. + * @throws {Error} If not initialized with an SVG element + */ + constructor (svgElem, optIdPrefix) { + if (!svgElem || !svgElem.tagName || !svgElem.namespaceURI || + svgElem.tagName !== 'svg' || svgElem.namespaceURI !== NS.SVG) { + throw new Error('Error: svgedit.draw.Drawing instance initialized without a element'); + } + + /** + * The SVG DOM Element that represents this drawing. + * @type {SVGSVGElement} + */ + this.svgElem_ = svgElem; + + /** + * The latest object number used in this drawing. + * @type {Integer} + */ + this.obj_num = 0; + + /** + * The prefix to prepend to each element id in the drawing. + * @type {string} + */ + this.idPrefix = optIdPrefix || 'svg_'; + + /** + * An array of released element ids to immediately reuse. + * @type {Integer[]} + */ + this.releasedNums = []; + + /** + * The z-ordered array of Layer objects. Each layer has a name + * and group element. + * The first layer is the one at the bottom of the rendering. + * @type {Layer[]} + */ + this.all_layers = []; + + /** + * Map of all_layers by name. + * + * Note: Layers are ordered, but referenced externally by name; so, we need both container + * types depending on which function is called (i.e. all_layers and layer_map). + * + * @type {PlainObject.} + */ + this.layer_map = {}; + + /** + * The current layer being used. + * @type {Layer} + */ + this.current_layer = null; + + /** + * The nonce to use to uniquely identify elements across drawings. + * @type {!String} + */ + this.nonce_ = ''; + const n = this.svgElem_.getAttributeNS(NS.SE, 'nonce'); + // If already set in the DOM, use the nonce throughout the document + // else, if randomizeIds(true) has been called, create and set the nonce. + if (!!n && randIds !== RandomizeModes.NEVER_RANDOMIZE) { + this.nonce_ = n; + } else if (randIds === RandomizeModes.ALWAYS_RANDOMIZE) { + this.setNonce(Math.floor(Math.random() * 100001)); + } + } + + /** + * @param {string} id Element ID to retrieve + * @returns {Element} SVG element within the root SVGSVGElement + */ + getElem_ (id) { + if (this.svgElem_.querySelector) { + // querySelector lookup + return this.svgElem_.querySelector('#' + id); + } + // jQuery lookup: twice as slow as xpath in FF + return $(this.svgElem_).find('[id=' + id + ']')[0]; + } + + /** + * @returns {SVGSVGElement} + */ + getSvgElem () { + return this.svgElem_; + } + + /** + * @returns {!(string|Integer)} The previously set nonce + */ + getNonce () { + return this.nonce_; + } + + /** + * @param {!(string|Integer)} n The nonce to set + * @returns {undefined} + */ + setNonce (n) { + this.svgElem_.setAttributeNS(NS.XMLNS, 'xmlns:se', NS.SE); + this.svgElem_.setAttributeNS(NS.SE, 'se:nonce', n); + this.nonce_ = n; + } + + /** + * Clears any previously set nonce + * @returns {undefined} + */ + clearNonce () { + // We deliberately leave any se:nonce attributes alone, + // we just don't use it to randomize ids. + this.nonce_ = ''; + } + + /** + * Returns the latest object id as a string. + * @returns {string} The latest object Id. + */ + getId () { + return this.nonce_ + ? this.idPrefix + this.nonce_ + '_' + this.obj_num + : this.idPrefix + this.obj_num; + } + + /** + * Returns the next object Id as a string. + * @returns {string} The next object Id to use. + */ + getNextId () { + const oldObjNum = this.obj_num; + let restoreOldObjNum = false; + + // If there are any released numbers in the release stack, + // use the last one instead of the next obj_num. + // We need to temporarily use obj_num as that is what getId() depends on. + if (this.releasedNums.length > 0) { + this.obj_num = this.releasedNums.pop(); + restoreOldObjNum = true; + } else { + // If we are not using a released id, then increment the obj_num. + this.obj_num++; + } + + // Ensure the ID does not exist. + let id = this.getId(); + while (this.getElem_(id)) { + if (restoreOldObjNum) { + this.obj_num = oldObjNum; + restoreOldObjNum = false; + } + this.obj_num++; + id = this.getId(); + } + // Restore the old object number if required. + if (restoreOldObjNum) { + this.obj_num = oldObjNum; + } + return id; + } + + /** + * Releases the object Id, letting it be used as the next id in getNextId(). + * This method DOES NOT remove any elements from the DOM, it is expected + * that client code will do this. + * @param {string} id - The id to release. + * @returns {boolean} True if the id was valid to be released, false otherwise. + */ + releaseId (id) { + // confirm if this is a valid id for this Document, else return false + const front = this.idPrefix + (this.nonce_ ? this.nonce_ + '_' : ''); + if (typeof id !== 'string' || !id.startsWith(front)) { + return false; + } + // extract the obj_num of this id + const num = parseInt(id.substr(front.length), 10); + + // if we didn't get a positive number or we already released this number + // then return false. + if (typeof num !== 'number' || num <= 0 || this.releasedNums.includes(num)) { + return false; + } + + // push the released number into the released queue + this.releasedNums.push(num); + + return true; + } + + /** + * Returns the number of layers in the current drawing. + * @returns {Integer} The number of layers in the current drawing. + */ + getNumLayers () { + return this.all_layers.length; + } + + /** + * Check if layer with given name already exists + * @param {string} name - The layer name to check + * @returns {boolean} + */ + hasLayer (name) { + return this.layer_map[name] !== undefined; + } + + /** + * Returns the name of the ith layer. If the index is out of range, an empty string is returned. + * @param {Integer} i - The zero-based index of the layer you are querying. + * @returns {string} The name of the ith layer (or the empty string if none found) + */ + getLayerName (i) { + return i >= 0 && i < this.getNumLayers() ? this.all_layers[i].getName() : ''; + } + + /** + * @returns {SVGGElement|null} The SVGGElement representing the current layer. + */ + getCurrentLayer () { + return this.current_layer ? this.current_layer.getGroup() : null; + } + + /** + * Get a layer by name. + * @returns {SVGGElement} The SVGGElement representing the named layer or null. + */ + getLayerByName (name) { + const layer = this.layer_map[name]; + return layer ? layer.getGroup() : null; + } + + /** + * Returns the name of the currently selected layer. If an error occurs, an empty string + * is returned. + * @returns {string} The name of the currently active layer (or the empty string if none found). + */ + getCurrentLayerName () { + return this.current_layer ? this.current_layer.getName() : ''; + } + + /** + * Set the current layer's name. + * @param {string} name - The new name. + * @param {module:history.HistoryRecordingService} hrService - History recording service + * @returns {string|null} The new name if changed; otherwise, null. + */ + setCurrentLayerName (name, hrService) { + let finalName = null; + if (this.current_layer) { + const oldName = this.current_layer.getName(); + finalName = this.current_layer.setName(name, hrService); + if (finalName) { + delete this.layer_map[oldName]; + this.layer_map[finalName] = this.current_layer; + } + } + return finalName; + } + + /** + * Set the current layer's position. + * @param {Integer} newpos - The zero-based index of the new position of the layer. Range should be 0 to layers-1 + * @returns {{title: SVGGElement, previousName: string}|null} If the name was changed, returns {title:SVGGElement, previousName:string}; otherwise null. + */ + setCurrentLayerPosition (newpos) { + const layerCount = this.getNumLayers(); + if (!this.current_layer || newpos < 0 || newpos >= layerCount) { + return null; + } + + let oldpos; + for (oldpos = 0; oldpos < layerCount; ++oldpos) { + if (this.all_layers[oldpos] === this.current_layer) { break; } + } + // some unknown error condition (current_layer not in all_layers) + if (oldpos === layerCount) { return null; } + + if (oldpos !== newpos) { + // if our new position is below us, we need to insert before the node after newpos + const currentGroup = this.current_layer.getGroup(); + const oldNextSibling = currentGroup.nextSibling; + + let refGroup = null; + if (newpos > oldpos) { + if (newpos < layerCount - 1) { + refGroup = this.all_layers[newpos + 1].getGroup(); + } + // if our new position is above us, we need to insert before the node at newpos + } else { + refGroup = this.all_layers[newpos].getGroup(); + } + this.svgElem_.insertBefore(currentGroup, refGroup); // Ok to replace with `refGroup.before(currentGroup);`? + + this.identifyLayers(); + this.setCurrentLayer(this.getLayerName(newpos)); + + return { + currentGroup, + oldNextSibling + }; + } + return null; + } + + /** + * @param {module:history.HistoryRecordingService} hrService + * @returns {undefined} + */ + mergeLayer (hrService) { + const currentGroup = this.current_layer.getGroup(); + const prevGroup = $(currentGroup).prev()[0]; + if (!prevGroup) { return; } + + hrService.startBatchCommand('Merge Layer'); + + const layerNextSibling = currentGroup.nextSibling; + hrService.removeElement(currentGroup, layerNextSibling, this.svgElem_); + + while (currentGroup.firstChild) { + const child = currentGroup.firstChild; + if (child.localName === 'title') { + hrService.removeElement(child, child.nextSibling, currentGroup); + child.remove(); + continue; + } + const oldNextSibling = child.nextSibling; + prevGroup.append(child); + hrService.moveElement(child, oldNextSibling, currentGroup); + } + + // Remove current layer's group + this.current_layer.removeGroup(); + // Remove the current layer and set the previous layer as the new current layer + const index = this.all_layers.indexOf(this.current_layer); + if (index > 0) { + const name = this.current_layer.getName(); + this.current_layer = this.all_layers[index - 1]; + this.all_layers.splice(index, 1); + delete this.layer_map[name]; + } + + hrService.endBatchCommand(); + } + + /** + * @param {module:history.HistoryRecordingService} hrService + * @returns {undefined} + */ + mergeAllLayers (hrService) { + // Set the current layer to the last layer. + this.current_layer = this.all_layers[this.all_layers.length - 1]; + + hrService.startBatchCommand('Merge all Layers'); + while (this.all_layers.length > 1) { + this.mergeLayer(hrService); + } + hrService.endBatchCommand(); + } + + /** + * Sets the current layer. If the name is not a valid layer name, then this + * function returns `false`. Otherwise it returns `true`. This is not an + * undo-able action. + * @param {string} name - The name of the layer you want to switch to. + * @returns {boolean} `true` if the current layer was switched, otherwise `false` + */ + setCurrentLayer (name) { + const layer = this.layer_map[name]; + if (layer) { + if (this.current_layer) { + this.current_layer.deactivate(); + } + this.current_layer = layer; + this.current_layer.activate(); + return true; + } + return false; + } + + /** + * Deletes the current layer from the drawing and then clears the selection. + * This function then calls the 'changed' handler. This is an undoable action. + * @todo Does this actually call the 'changed' handler? + * @returns {SVGGElement} The SVGGElement of the layer removed or null. + */ + deleteCurrentLayer () { + if (this.current_layer && this.getNumLayers() > 1) { + const oldLayerGroup = this.current_layer.removeGroup(); + this.identifyLayers(); + return oldLayerGroup; + } + return null; + } + + /** + * Updates layer system and sets the current layer to the + * top-most layer (last `` child of this drawing). + * @returns {undefined} + */ + identifyLayers () { + this.all_layers = []; + this.layer_map = {}; + const numchildren = this.svgElem_.childNodes.length; + // loop through all children of SVG element + const orphans = [], layernames = []; + let layer = null; + let childgroups = false; + for (let i = 0; i < numchildren; ++i) { + const child = this.svgElem_.childNodes.item(i); + // for each g, find its layer name + if (child && child.nodeType === 1) { + if (child.tagName === 'g') { + childgroups = true; + const name = findLayerNameInGroup(child); + if (name) { + layernames.push(name); + layer = new Layer(name, child); + this.all_layers.push(layer); + this.layer_map[name] = layer; + } else { + // if group did not have a name, it is an orphan + orphans.push(child); + } + } else if (visElems.includes(child.nodeName)) { + // Child is "visible" (i.e. not a or element), so it is an orphan + orphans.push(child); + } + } + } + + // If orphans or no layers found, create a new layer and add all the orphans to it + if (orphans.length > 0 || !childgroups) { + layer = new Layer(getNewLayerName(layernames), null, this.svgElem_); + layer.appendChildren(orphans); + this.all_layers.push(layer); + this.layer_map[name] = layer; + } else { + layer.activate(); + } + this.current_layer = layer; + } + + /** + * Creates a new top-level layer in the drawing with the given name and + * makes it the current layer. + * @param {string} name - The given name. If the layer name exists, a new name will be generated. + * @param {module:history.HistoryRecordingService} hrService - History recording service + * @returns {SVGGElement} The SVGGElement of the new layer, which is + * also the current layer of this drawing. + */ + createLayer (name, hrService) { + if (this.current_layer) { + this.current_layer.deactivate(); + } + // Check for duplicate name. + if (name === undefined || name === null || name === '' || this.layer_map[name]) { + name = getNewLayerName(Object.keys(this.layer_map)); + } + + // Crate new layer and add to DOM as last layer + const layer = new Layer(name, null, this.svgElem_); + // Like to assume hrService exists, but this is backwards compatible with old version of createLayer. + if (hrService) { + hrService.startBatchCommand('Create Layer'); + hrService.insertElement(layer.getGroup()); + hrService.endBatchCommand(); + } + + this.all_layers.push(layer); + this.layer_map[name] = layer; + this.current_layer = layer; + return layer.getGroup(); + } + + /** + * Creates a copy of the current layer with the given name and makes it the current layer. + * @param {string} name - The given name. If the layer name exists, a new name will be generated. + * @param {module:history.HistoryRecordingService} hrService - History recording service + * @returns {SVGGElement} The SVGGElement of the new layer, which is + * also the current layer of this drawing. + */ + cloneLayer (name, hrService) { + if (!this.current_layer) { return null; } + this.current_layer.deactivate(); + // Check for duplicate name. + if (name === undefined || name === null || name === '' || this.layer_map[name]) { + name = getNewLayerName(Object.keys(this.layer_map)); + } + + // Create new group and add to DOM just after current_layer + const currentGroup = this.current_layer.getGroup(); + const layer = new Layer(name, currentGroup, this.svgElem_); + const group = layer.getGroup(); + + // Clone children + const children = currentGroup.childNodes; + for (let index = 0; index < children.length; index++) { + const ch = children[index]; + if (ch.localName === 'title') { continue; } + group.append(this.copyElem(ch)); + } + + if (hrService) { + hrService.startBatchCommand('Duplicate Layer'); + hrService.insertElement(group); + hrService.endBatchCommand(); + } + + // Update layer containers and current_layer. + const index = this.all_layers.indexOf(this.current_layer); + if (index >= 0) { + this.all_layers.splice(index + 1, 0, layer); + } else { + this.all_layers.push(layer); + } + this.layer_map[name] = layer; + this.current_layer = layer; + return group; + } + + /** + * Returns whether the layer is visible. If the layer name is not valid, + * then this function returns `false`. + * @param {string} layerName - The name of the layer which you want to query. + * @returns {boolean} The visibility state of the layer, or `false` if the layer name was invalid. + */ + getLayerVisibility (layerName) { + const layer = this.layer_map[layerName]; + return layer ? layer.isVisible() : false; + } + + /** + * Sets the visibility of the layer. If the layer name is not valid, this + * function returns `null`, otherwise it returns the `SVGElement` representing + * the layer. This is an undo-able action. + * @param {string} layerName - The name of the layer to change the visibility + * @param {boolean} bVisible - Whether the layer should be visible + * @returns {?SVGGElement} The SVGGElement representing the layer if the + * `layerName` was valid, otherwise `null`. + */ + setLayerVisibility (layerName, bVisible) { + if (typeof bVisible !== 'boolean') { + return null; + } + const layer = this.layer_map[layerName]; + if (!layer) { return null; } + layer.setVisible(bVisible); + return layer.getGroup(); + } + + /** + * Returns the opacity of the given layer. If the input name is not a layer, `null` is returned. + * @param {string} layerName - name of the layer on which to get the opacity + * @returns {?Float} The opacity value of the given layer. This will be a value between 0.0 and 1.0, or `null` + * if `layerName` is not a valid layer + */ + getLayerOpacity (layerName) { + const layer = this.layer_map[layerName]; + if (!layer) { return null; } + return layer.getOpacity(); + } + + /** + * Sets the opacity of the given layer. If the input name is not a layer, + * nothing happens. If opacity is not a value between 0.0 and 1.0, then + * nothing happens. + * NOTE: this function exists solely to apply a highlighting/de-emphasis + * effect to a layer. When it is possible for a user to affect the opacity + * of a layer, we will need to allow this function to produce an undo-able + * action. + * @param {string} layerName - Name of the layer on which to set the opacity + * @param {Float} opacity - A float value in the range 0.0-1.0 + * @returns {undefined} + */ + setLayerOpacity (layerName, opacity) { + if (typeof opacity !== 'number' || opacity < 0.0 || opacity > 1.0) { + return; + } + const layer = this.layer_map[layerName]; + if (layer) { + layer.setOpacity(opacity); + } + } + + /** + * Create a clone of an element, updating its ID and its children's IDs when needed + * @param {Element} el - DOM element to clone + * @returns {Element} + */ + copyElem (el) { + const self = this; + const getNextIdClosure = function () { return self.getNextId(); }; + return utilCopyElem(el, getNextIdClosure); + } +} /** - * Creates a new top-level layer in the drawing with the given name and - * makes it the current layer. - * @param {string} name - The given name. If the layer name exists, a new name will be generated. - * @param {svgedit.history.HistoryRecordingService} hrService - History recording service - * @returns {SVGGElement} The SVGGElement of the new layer, which is - * also the current layer of this drawing. -*/ -svgedit.draw.Drawing.prototype.createLayer = function(name, hrService) { - if (this.current_layer) { - this.current_layer.deactivate(); - } - // Check for duplicate name. - if (name === undefined || name === null || name === '' || this.layer_map[name]) { - name = getNewLayerName(Object.keys(this.layer_map)); - } + * Called to ensure that drawings will or will not have randomized ids. + * The currentDrawing will have its nonce set if it doesn't already. + * @function module:draw.randomizeIds + * @param {boolean} enableRandomization - flag indicating if documents should have randomized ids + * @param {draw.Drawing} currentDrawing + * @returns {undefined} + */ +export const randomizeIds = function (enableRandomization, currentDrawing) { + randIds = enableRandomization === false + ? RandomizeModes.NEVER_RANDOMIZE + : RandomizeModes.ALWAYS_RANDOMIZE; - // Crate new layer and add to DOM as last layer - var layer = new svgedit.draw.Layer(name, null, this.svgElem_); - // Like to assume hrService exists, but this is backwards compatible with old version of createLayer. - if (hrService) { - hrService.startBatchCommand('Create Layer'); - hrService.insertElement(layer.getGroup()); - hrService.endBatchCommand(); - } - - this.all_layers.push(layer); - this.layer_map[name] = layer; - this.current_layer = layer; - return layer.getGroup(); + if (randIds === RandomizeModes.ALWAYS_RANDOMIZE && !currentDrawing.getNonce()) { + currentDrawing.setNonce(Math.floor(Math.random() * 100001)); + } else if (randIds === RandomizeModes.NEVER_RANDOMIZE && currentDrawing.getNonce()) { + currentDrawing.clearNonce(); + } }; +// Layer API Functions + /** - * Creates a copy of the current layer with the given name and makes it the current layer. - * @param {string} name - The given name. If the layer name exists, a new name will be generated. - * @param {svgedit.history.HistoryRecordingService} hrService - History recording service - * @returns {SVGGElement} The SVGGElement of the new layer, which is - * also the current layer of this drawing. +* Group: Layers */ -svgedit.draw.Drawing.prototype.cloneLayer = function(name, hrService) { - if (!this.current_layer) {return null;} - this.current_layer.deactivate(); - // Check for duplicate name. - if (name === undefined || name === null || name === '' || this.layer_map[name]) { - name = getNewLayerName(Object.keys(this.layer_map)); - } - - // Create new group and add to DOM just after current_layer - var currentGroup = this.current_layer.getGroup(); - var layer = new svgedit.draw.Layer(name, currentGroup, this.svgElem_); - var group = layer.getGroup(); - - // Clone children - var children = currentGroup.childNodes; - var index; - for (index = 0; index < children.length; index++) { - var ch = children[index]; - if (ch.localName == 'title') {continue;} - group.appendChild(this.copyElem(ch)); - } - - if (hrService) { - hrService.startBatchCommand('Duplicate Layer'); - hrService.insertElement(group); - hrService.endBatchCommand(); - } - - // Update layer containers and current_layer. - index = this.all_layers.indexOf(this.current_layer); - if (index >= 0) { - this.all_layers.splice(index + 1, 0, layer); - } else { - this.all_layers.push(layer); - } - this.layer_map[name] = layer; - this.current_layer = layer; - return group; -}; /** - * Returns whether the layer is visible. If the layer name is not valid, - * then this function returns false. - * @param {string} layername - The name of the layer which you want to query. - * @returns {boolean} The visibility state of the layer, or false if the layer name was invalid. -*/ -svgedit.draw.Drawing.prototype.getLayerVisibility = function(layername) { - var layer = this.layer_map[layername]; - return layer ? layer.isVisible() : false; -}; + * @see {@link https://api.jquery.com/jQuery.data/} + * @name external:jQuery.data + */ /** - * Sets the visibility of the layer. If the layer name is not valid, this - * function returns false, otherwise it returns true. This is an - * undo-able action. - * @param {string} layername - The name of the layer to change the visibility - * @param {boolean} bVisible - Whether the layer should be visible - * @returns {?SVGGElement} The SVGGElement representing the layer if the - * layername was valid, otherwise null. -*/ -svgedit.draw.Drawing.prototype.setLayerVisibility = function(layername, bVisible) { - if (typeof bVisible !== 'boolean') { - return null; - } - var layer = this.layer_map[layername]; - if (!layer) {return null;} - layer.setVisible(bVisible); - return layer.getGroup(); -}; - - + * @interface module:draw.DrawCanvasInit + * @property {module:path.pathActions} pathActions + * @property {external:jQuery.data} elData + * @property {module:history.UndoManager} undoMgr + */ /** - * Returns the opacity of the given layer. If the input name is not a layer, null is returned. - * @param {string} layername - name of the layer on which to get the opacity - * @returns {?number} The opacity value of the given layer. This will be a value between 0.0 and 1.0, or null - * if layername is not a valid layer -*/ -svgedit.draw.Drawing.prototype.getLayerOpacity = function(layername) { - var layer = this.layer_map[layername]; - if (!layer) {return null;} - return layer.getOpacity(); -}; - -/** - * Sets the opacity of the given layer. If the input name is not a layer, - * nothing happens. If opacity is not a value between 0.0 and 1.0, then - * nothing happens. - * @param {string} layername - Name of the layer on which to set the opacity - * @param {number} opacity - A float value in the range 0.0-1.0 -*/ -svgedit.draw.Drawing.prototype.setLayerOpacity = function(layername, opacity) { - if (typeof opacity !== 'number' || opacity < 0.0 || opacity > 1.0) { - return; - } - var layer = this.layer_map[layername]; - if (layer) { - layer.setOpacity(opacity); - } -}; - -/** - * Create a clone of an element, updating its ID and its children's IDs when needed - * @param {Element} el - DOM element to clone + * @function module:draw.DrawCanvasInit#getCurrentGroup * @returns {Element} */ -svgedit.draw.Drawing.prototype.copyElem = function(el) { - var self = this; - var getNextIdClosure = function() { return self.getNextId();} - return svgedit.utilities.copyElem(el, getNextIdClosure) -} +/** + * @function module:draw.DrawCanvasInit#setCurrentGroup + * @param {Element} cg + * @returns {undefined} +*/ +/** + * @function module:draw.DrawCanvasInit#getSelectedElements + * @returns {Element[]} the array with selected DOM elements +*/ +/** + * @function module:draw.DrawCanvasInit#getSVGContent + * @returns {SVGSVGElement} + */ +/** + * @function module:draw.DrawCanvasInit#getCurrentDrawing + * @returns {module:draw.Drawing} + */ +/** + * @function module:draw.DrawCanvasInit#clearSelection + * @param {boolean} [noCall] - When `true`, does not call the "selected" handler + * @returns {undefined} +*/ +/** + * Run the callback function associated with the given event + * @function module:draw.DrawCanvasInit#call + * @param {"changed"|"contextset"} ev - String with the event name + * @param {module:svgcanvas.SvgCanvas#event:changed|module:svgcanvas.SvgCanvas#event:contextset} arg - Argument to pass through to the callback + * function. If the event is "changed", a (single-item) array of `Element`s is + * passed. If the event is "contextset", the arg is `null` or `Element`. + * @returns {undefined} + */ +/** + * @function module:draw.DrawCanvasInit#addCommandToHistory + * @param {Command} cmd + * @returns {undefined} +*/ +/** + * @function module:draw.DrawCanvasInit#changeSVGContent + * @returns {undefined} + */ +let canvas_; +/** +* @function module:draw.init +* @param {module:draw.DrawCanvasInit} canvas +* @returns {undefined} +*/ +export const init = function (canvas) { + canvas_ = canvas; +}; -}()); +/** +* Updates layer system +* @function module:draw.identifyLayers +* @returns {undefined} +*/ +export const identifyLayers = function () { + leaveContext(); + canvas_.getCurrentDrawing().identifyLayers(); +}; + +/** +* Creates a new top-level layer in the drawing with the given name, sets the current layer +* to it, and then clears the selection. This function then calls the 'changed' handler. +* This is an undoable action. +* @function module:draw.createLayer +* @param {string} name - The given name +* @param {module:history.HistoryRecordingService} hrService +* @fires module:svgcanvas.SvgCanvas#event:changed +* @returns {undefined} +*/ +export const createLayer = function (name, hrService) { + const newLayer = canvas_.getCurrentDrawing().createLayer( + name, + historyRecordingService(hrService) + ); + canvas_.clearSelection(); + canvas_.call('changed', [newLayer]); +}; + +/** + * Creates a new top-level layer in the drawing with the given name, copies all the current layer's contents + * to it, and then clears the selection. This function then calls the 'changed' handler. + * This is an undoable action. + * @function module:draw.cloneLayer + * @param {string} name - The given name. If the layer name exists, a new name will be generated. + * @param {module:history.HistoryRecordingService} hrService - History recording service + * @fires module:svgcanvas.SvgCanvas#event:changed + * @returns {undefined} + */ +export const cloneLayer = function (name, hrService) { + // Clone the current layer and make the cloned layer the new current layer + const newLayer = canvas_.getCurrentDrawing().cloneLayer(name, historyRecordingService(hrService)); + + canvas_.clearSelection(); + leaveContext(); + canvas_.call('changed', [newLayer]); +}; + +/** +* Deletes the current layer from the drawing and then clears the selection. This function +* then calls the 'changed' handler. This is an undoable action. +* @function module:draw.deleteCurrentLayer +* @fires module:svgcanvas.SvgCanvas#event:changed +* @returns {boolean} `true` if an old layer group was found to delete +*/ +export const deleteCurrentLayer = function () { + let currentLayer = canvas_.getCurrentDrawing().getCurrentLayer(); + const {nextSibling} = currentLayer; + const parent = currentLayer.parentNode; + currentLayer = canvas_.getCurrentDrawing().deleteCurrentLayer(); + if (currentLayer) { + const batchCmd = new BatchCommand('Delete Layer'); + // store in our Undo History + batchCmd.addSubCommand(new RemoveElementCommand(currentLayer, nextSibling, parent)); + canvas_.addCommandToHistory(batchCmd); + canvas_.clearSelection(); + canvas_.call('changed', [parent]); + return true; + } + return false; +}; + +/** +* Sets the current layer. If the name is not a valid layer name, then this function returns +* false. Otherwise it returns true. This is not an undo-able action. +* @function module:draw.setCurrentLayer +* @param {string} name - The name of the layer you want to switch to. +* @returns {boolean} true if the current layer was switched, otherwise false +*/ +export const setCurrentLayer = function (name) { + const result = canvas_.getCurrentDrawing().setCurrentLayer(toXml(name)); + if (result) { + canvas_.clearSelection(); + } + return result; +}; + +/** +* Renames the current layer. If the layer name is not valid (i.e. unique), then this function +* does nothing and returns `false`, otherwise it returns `true`. This is an undo-able action. +* @function module:draw.renameCurrentLayer +* @param {string} newName - the new name you want to give the current layer. This name must +* be unique among all layer names. +* @fires module:svgcanvas.SvgCanvas#event:changed +* @returns {boolean} Whether the rename succeeded +*/ +export const renameCurrentLayer = function (newName) { + const drawing = canvas_.getCurrentDrawing(); + const layer = drawing.getCurrentLayer(); + if (layer) { + const result = drawing.setCurrentLayerName(newName, historyRecordingService()); + if (result) { + canvas_.call('changed', [layer]); + return true; + } + } + return false; +}; + +/** +* Changes the position of the current layer to the new value. If the new index is not valid, +* this function does nothing and returns false, otherwise it returns true. This is an +* undo-able action. +* @function module:draw.setCurrentLayerPosition +* @param {Integer} newPos - The zero-based index of the new position of the layer. This should be between +* 0 and (number of layers - 1) +* @returns {boolean} `true` if the current layer position was changed, `false` otherwise. +*/ +export const setCurrentLayerPosition = function (newPos) { + const drawing = canvas_.getCurrentDrawing(); + const result = drawing.setCurrentLayerPosition(newPos); + if (result) { + canvas_.addCommandToHistory(new MoveElementCommand(result.currentGroup, result.oldNextSibling, canvas_.getSVGContent())); + return true; + } + return false; +}; + +/** +* Sets the visibility of the layer. If the layer name is not valid, this function return +* `false`, otherwise it returns `true`. This is an undo-able action. +* @function module:draw.setLayerVisibility +* @param {string} layerName - The name of the layer to change the visibility +* @param {boolean} bVisible - Whether the layer should be visible +* @returns {boolean} true if the layer's visibility was set, false otherwise +*/ +export const setLayerVisibility = function (layerName, bVisible) { + const drawing = canvas_.getCurrentDrawing(); + const prevVisibility = drawing.getLayerVisibility(layerName); + const layer = drawing.setLayerVisibility(layerName, bVisible); + if (layer) { + const oldDisplay = prevVisibility ? 'inline' : 'none'; + canvas_.addCommandToHistory(new ChangeElementCommand(layer, {display: oldDisplay}, 'Layer Visibility')); + } else { + return false; + } + + if (layer === drawing.getCurrentLayer()) { + canvas_.clearSelection(); + canvas_.pathActions.clear(); + } + // call('changed', [selected]); + return true; +}; + +/** +* Moves the selected elements to layerName. If the name is not a valid layer name, then `false` +* is returned. Otherwise it returns `true`. This is an undo-able action. +* @function module:draw.moveSelectedToLayer +* @param {string} layerName - The name of the layer you want to which you want to move the selected elements +* @returns {boolean} Whether the selected elements were moved to the layer. +*/ +export const moveSelectedToLayer = function (layerName) { + // find the layer + const drawing = canvas_.getCurrentDrawing(); + const layer = drawing.getLayerByName(layerName); + if (!layer) { return false; } + + const batchCmd = new BatchCommand('Move Elements to Layer'); + + // loop for each selected element and move it + const selElems = canvas_.getSelectedElements(); + let i = selElems.length; + while (i--) { + const elem = selElems[i]; + if (!elem) { continue; } + const oldNextSibling = elem.nextSibling; + // TODO: this is pretty brittle! + const oldLayer = elem.parentNode; + layer.append(elem); + batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldLayer)); + } + + canvas_.addCommandToHistory(batchCmd); + + return true; +}; + +/** +* @function module:draw.mergeLayer +* @param {module:history.HistoryRecordingService} hrService +* @returns {undefined} +*/ +export const mergeLayer = function (hrService) { + canvas_.getCurrentDrawing().mergeLayer(historyRecordingService(hrService)); + canvas_.clearSelection(); + leaveContext(); + canvas_.changeSVGContent(); +}; + +/** +* @function module:draw.mergeAllLayers +* @param {module:history.HistoryRecordingService} hrService +* @returns {undefined} +*/ +export const mergeAllLayers = function (hrService) { + canvas_.getCurrentDrawing().mergeAllLayers(historyRecordingService(hrService)); + canvas_.clearSelection(); + leaveContext(); + canvas_.changeSVGContent(); +}; + +/** +* Return from a group context to the regular kind, make any previously +* disabled elements enabled again +* @function module:draw.leaveContext +* @fires module:svgcanvas.SvgCanvas#event:contextset +* @returns {undefined} +*/ +export const leaveContext = function () { + const len = disabledElems.length; + if (len) { + for (let i = 0; i < len; i++) { + const elem = disabledElems[i]; + const orig = canvas_.elData(elem, 'orig_opac'); + if (orig !== 1) { + elem.setAttribute('opacity', orig); + } else { + elem.removeAttribute('opacity'); + } + elem.setAttribute('style', 'pointer-events: inherit'); + } + disabledElems = []; + canvas_.clearSelection(true); + canvas_.call('contextset', null); + } + canvas_.setCurrentGroup(null); +}; + +/** +* Set the current context (for in-group editing) +* @function module:draw.setContext +* @param {Element} elem +* @fires module:svgcanvas.SvgCanvas#event:contextset +* @returns {undefined} +*/ +export const setContext = function (elem) { + leaveContext(); + if (typeof elem === 'string') { + elem = getElem(elem); + } + + // Edit inside this group + canvas_.setCurrentGroup(elem); + + // Disable other elements + $(elem).parentsUntil('#svgcontent').andSelf().siblings().each(function () { + const opac = this.getAttribute('opacity') || 1; + // Store the original's opacity + canvas_.elData(this, 'orig_opac', opac); + this.setAttribute('opacity', opac * 0.33); + this.setAttribute('style', 'pointer-events: none'); + disabledElems.push(this); + }); + + canvas_.clearSelection(); + canvas_.call('contextset', canvas_.getCurrentGroup()); +}; + +/** +* @memberof module:draw +* @class Layer +* @see {@link module:layer.Layer} +*/ +export {Layer}; diff --git a/editor/embedapi-dom.js b/editor/embedapi-dom.js index dc5afcd2..8a0a7867 100644 --- a/editor/embedapi-dom.js +++ b/editor/embedapi-dom.js @@ -1,79 +1,91 @@ -/*globals $, EmbeddedSVGEdit*/ -/*jslint vars: true */ -var initEmbed; +/* globals jQuery */ +/** +* Attaches items to DOM for Embedded SVG support +* @module EmbeddedSVGEditDOM +*/ +import EmbeddedSVGEdit from './embedapi.js'; -// Todo: Get rid of frame.contentWindow dependencies so can be more easily adjusted to work cross-domain +const $ = jQuery; -$(function () {'use strict'; - - var svgCanvas = null; - var frame; +let svgCanvas = null; - initEmbed = function () { - var doc, mainButton; - svgCanvas = new EmbeddedSVGEdit(frame); - // Hide main button, as we will be controlling new, load, save, etc. from the host document - doc = frame.contentDocument || frame.contentWindow.document; - mainButton = doc.getElementById('main_button'); - mainButton.style.display = 'none'; - }; +function handleSvgData (data, error) { + if (error) { + alert('error ' + error); + } else { + alert('Congratulations. Your SVG string is back in the host page, do with it what you will\n\n' + data); + } +} - function handleSvgData(data, error) { - if (error) { - alert('error ' + error); - } else { - alert('Congratulations. Your SVG string is back in the host page, do with it what you will\n\n' + data); - } - } +function loadSvg () { + const svgexample = 'Layer 1'; + svgCanvas.setSvgString(svgexample); +} - function loadSvg() { - var svgexample = 'Layer 1'; - svgCanvas.setSvgString(svgexample); - } +function saveSvg () { + svgCanvas.getSvgString()(handleSvgData); +} - function saveSvg() { - svgCanvas.getSvgString()(handleSvgData); - } - - function exportPNG() { - var str = frame.contentWindow.svgEditor.uiStrings.notification.loadingImage; +function exportPNG () { + svgCanvas.getUIStrings()(function (uiStrings) { + const str = uiStrings.notification.loadingImage; - var exportWindow = window.open( - 'data:text/html;charset=utf-8,' + encodeURIComponent('' + str + '

    ' + str + '

    '), - 'svg-edit-exportWindow' - ); - svgCanvas.rasterExport('PNG', null, exportWindow.name); - } - - function exportPDF() { - var str = frame.contentWindow.svgEditor.uiStrings.notification.loadingImage; - - /** - // If you want to handle the PDF blob yourself, do as follows - svgCanvas.bind('exportedPDF', function (win, data) { - alert(data.dataurlstring); - }); - svgCanvas.exportPDF(); // Accepts two args: optionalWindowName supplied back to bound exportPDF handler and optionalOutputType (defaults to dataurlstring) - return; - */ - - var exportWindow = window.open( - 'data:text/html;charset=utf-8,' + encodeURIComponent('' + str + '

    ' + str + '

    '), - 'svg-edit-exportWindow' - ); - svgCanvas.exportPDF(exportWindow.name); - } - - // Add event handlers - $('#load').click(loadSvg); - $('#save').click(saveSvg); - $('#exportPNG').click(exportPNG); - $('#exportPDF').click(exportPDF); - $('body').append( - $('' - ) + const exportWindow = window.open( + 'data:text/html;charset=utf-8,' + encodeURIComponent('' + str + '

    ' + str + '

    '), + 'svg-edit-exportWindow' ); - frame = document.getElementById('svgedit'); + svgCanvas.rasterExport('PNG', null, exportWindow && exportWindow.name); + }); +} + +function exportPDF () { + svgCanvas.getUIStrings()(function (uiStrings) { + const str = uiStrings.notification.loadingImage; + + /** + // If you want to handle the PDF blob yourself, do as follows + svgCanvas.bind('exportedPDF', function (win, data) { + alert(data.output); + }); + svgCanvas.exportPDF(); // Accepts two args: optionalWindowName supplied back to bound exportPDF handler and optional outputType (defaults to dataurlstring) + return; + */ + + const exportWindow = window.open( + 'data:text/html;charset=utf-8,' + encodeURIComponent('' + str + '

    ' + str + '

    '), + 'svg-edit-exportWindow' + ); + svgCanvas.exportPDF(exportWindow && exportWindow.name); + }); +} + +// Add event handlers +$('#load').click(loadSvg); +$('#save').click(saveSvg); +$('#exportPNG').click(exportPNG); +$('#exportPDF').click(exportPDF); + +const frameBase = 'https://raw.githack.com/SVG-Edit/svgedit/master'; +// const frameBase = 'http://localhost:8001'; +const framePath = '/editor/xdomain-svg-editor-es.html?extensions=ext-xdomain-messaging.js'; +const iframe = $(`' +); +iframe[0].addEventListener('load', function () { + svgCanvas = new EmbeddedSVGEdit(frame, [new URL(frameBase).origin]); + // Hide main button, as we will be controlling new, load, save, etc. from the host document + let doc; + try { + doc = frame.contentDocument || frame.contentWindow.document; + } catch (err) { + console.log('Blocked from accessing document'); + return; + } + const mainButton = doc.getElementById('main_button'); + mainButton.style.display = 'none'; }); +$('body').append(iframe); +const frame = document.getElementById('svgedit'); diff --git a/editor/embedapi.html b/editor/embedapi.html index 44ee3308..dda45a19 100644 --- a/editor/embedapi.html +++ b/editor/embedapi.html @@ -1,17 +1,17 @@ - - + + - - Embed API - - - + + Embed API + + + - - - - -
    + + + + +
    diff --git a/editor/embedapi.js b/editor/embedapi.js index b50a896b..625479b0 100644 --- a/editor/embedapi.js +++ b/editor/embedapi.js @@ -1,179 +1,391 @@ -/* -Embedded SVG-edit API - -General usage: -- Have an iframe somewhere pointing to a version of svg-edit > r1000 -- Initialize the magic with: -var svgCanvas = new EmbeddedSVGEdit(window.frames.svgedit); -- Pass functions in this format: -svgCanvas.setSvgString('string') -- Or if a callback is needed: -svgCanvas.setSvgString('string')(function(data, error){ - if (error){ - // There was an error - } else{ - // Handle data - } -}) - -Everything is done with the same API as the real svg-edit, -and all documentation is unchanged. - -However, this file depends on the postMessage API which -can only support JSON-serializable arguments and -return values, so, for example, arguments whose value is -'undefined', a function, a non-finite number, or a built-in -object like Date(), RegExp(), etc. will most likely not behave -as expected. In such a case one may need to host -the SVG editor on the same domain and reference the -JavaScript methods on the frame itself. - -The only other difference is -when handling returns: the callback notation is used instead. - -var blah = new EmbeddedSVGEdit(window.frames.svgedit); -blah.clearSelection('woot', 'blah', 1337, [1, 2, 3, 4, 5, 'moo'], -42, {a: 'tree',b:6, c: 9})(function(){console.log('GET DATA',arguments)}) +/** +* Handles underlying communication between the embedding window and the editor frame +* @module EmbeddedSVGEdit */ -(function () {'use strict'; +let cbid = 0; -var cbid = 0; +/** +* @callback module:EmbeddedSVGEdit.GenericCallback +* @param {...*} args Signature dependent on the function +* @returns {*} Return dependent on the function +*/ +/** +* @callback module:EmbeddedSVGEdit.CallbackSetter +* @param {module:EmbeddedSVGEdit.GenericCallback} newCallback Callback to be stored (signature dependent on function) +* @returns {undefined} +*/ +/** +* @callback module:EmbeddedSVGEdit.CallbackSetGetter +* @param {...*} args Signature dependent on the function +* @returns {module:EmbeddedSVGEdit.CallbackSetter} +*/ -function getCallbackSetter (d) { - return function () { - var t = this, // New callback - args = [].slice.call(arguments), - cbid = t.send(d, args, function(){}); // The callback (currently it's nothing, but will be set later) +/** +* @param {string} d +* @returns {module:EmbeddedSVGEdit.CallbackSetGetter} +*/ +function getCallbackSetter (funcName) { + return function (...args) { + const t = this, // New callback + cbid = t.send(funcName, args, function () {}); // The callback (currently it's nothing, but will be set later) - return function(newcallback){ - t.callbacks[cbid] = newcallback; // Set callback - }; + return function (newCallback) { + t.callbacks[cbid] = newCallback; // Set callback + }; }; } -/* +/** * Having this separate from messageListener allows us to * avoid using JSON parsing (and its limitations) in the case * of same domain control +* @param {module:EmbeddedSVGEdit.EmbeddedSVGEdit} t The `this` value +* @param {JSON} data +* @returns {undefined} */ function addCallback (t, data) { - var result = data.result || data.error, - cbid = data.id; + const result = data.result || data.error, + cbid = data.id; if (t.callbacks[cbid]) { - if (data.result) { - t.callbacks[cbid](result); - } else { - t.callbacks[cbid](result, 'error'); - } + if (data.result) { + t.callbacks[cbid](result); + } else { + t.callbacks[cbid](result, 'error'); + } } } +/** +* @param {Event} e +* @returns {undefined} +*/ function messageListener (e) { // We accept and post strings as opposed to objects for the sake of IE9 support; this // will most likely be changed in the future if (typeof e.data !== 'string') { - return; + return; } - var allowedOrigins = this.allowedOrigins, - data = e.data && JSON.parse(e.data); + const {allowedOrigins} = this, + data = e.data && JSON.parse(e.data); if (!data || typeof data !== 'object' || data.namespace !== 'svg-edit' || - e.source !== this.frame.contentWindow || - (allowedOrigins.indexOf('*') === -1 && allowedOrigins.indexOf(e.origin) === -1) + e.source !== this.frame.contentWindow || + (!allowedOrigins.includes('*') && !allowedOrigins.includes(e.origin)) ) { - return; + console.log(`The origin ${e.origin} was not whitelisted as an origin from which responses may be received by this ${window.origin} script.`); + return; } addCallback(this, data); } +/** +* @callback module:EmbeddedSVGEdit.MessageListener +* @param {MessageEvent} e +* @returns {undefined} +*/ +/** +* @param {module:EmbeddedSVGEdit.EmbeddedSVGEdit} t The `this` value +* @returns {module:EmbeddedSVGEdit.MessageListener} Event listener +*/ function getMessageListener (t) { - return function (e) { - messageListener.call(t, e); - }; + return function (e) { + messageListener.call(t, e); + }; } /** -* @param {HTMLIFrameElement} frame -* @param {array} [allowedOrigins=[]] Array of origins from which incoming -* messages will be allowed when same origin is not used; defaults to none. -* If supplied, it should probably be the same as svgEditor's allowedOrigins +* Embedded SVG-edit API +* General usage: +- Have an iframe somewhere pointing to a version of svg-edit > r1000 +* @example + +// Initialize the magic with: +const svgCanvas = new EmbeddedSVGEdit(window.frames.svgedit); + +// Pass functions in this format: +svgCanvas.setSvgString('string'); + +// Or if a callback is needed: +svgCanvas.setSvgString('string')(function(data, error){ + if (error){ + // There was an error + } else{ + // Handle data + } +}); + +// Everything is done with the same API as the real svg-edit, +// and all documentation is unchanged. + +// However, this file depends on the postMessage API which +// can only support JSON-serializable arguments and +// return values, so, for example, arguments whose value is +// 'undefined', a function, a non-finite number, or a built-in +// object like Date(), RegExp(), etc. will most likely not behave +// as expected. In such a case one may need to host +// the SVG editor on the same domain and reference the +// JavaScript methods on the frame itself. + +// The only other difference is +// when handling returns: the callback notation is used instead. +const blah = new EmbeddedSVGEdit(window.frames.svgedit); +blah.clearSelection('woot', 'blah', 1337, [1, 2, 3, 4, 5, 'moo'], -42, {a: 'tree',b:6, c: 9})(function(){console.log('GET DATA',arguments)}) +* +* @memberof module:EmbeddedSVGEdit */ -function EmbeddedSVGEdit (frame, allowedOrigins) { - if (!(this instanceof EmbeddedSVGEdit)) { // Allow invocation without 'new' keyword - return new EmbeddedSVGEdit(frame); +class EmbeddedSVGEdit { + /** + * @param {HTMLIFrameElement} frame + * @param {string[]} [allowedOrigins=[]] Array of origins from which incoming + * messages will be allowed when same origin is not used; defaults to none. + * If supplied, it should probably be the same as svgEditor's allowedOrigins + */ + constructor (frame, allowedOrigins) { + const t = this; + this.allowedOrigins = allowedOrigins || []; + // Initialize communication + this.frame = frame; + this.callbacks = {}; + // List of functions extracted with this: + // Run in firebug on http://svg-edit.googlecode.com/svn/trunk/docs/files/svgcanvas-js.html + + // for (const i=0,q=[],f = document.querySelectorAll('div.CFunction h3.CTitle a'); i < f.length; i++) { q.push(f[i].name); }; q + // const functions = ['clearSelection', 'addToSelection', 'removeFromSelection', 'open', 'save', 'getSvgString', 'setSvgString', + // 'createLayer', 'deleteCurrentLayer', 'setCurrentLayer', 'renameCurrentLayer', 'setCurrentLayerPosition', 'setLayerVisibility', + // 'moveSelectedToLayer', 'clear']; + + // Newer, well, it extracts things that aren't documented as well. All functions accessible through the normal thingy can now be accessed though the API + // const {svgCanvas} = frame.contentWindow; + // const l = []; + // for (const i in svgCanvas) { if (typeof svgCanvas[i] === 'function') { l.push(i);} }; + // alert("['" + l.join("', '") + "']"); + // Run in svgedit itself + const functions = [ + 'addExtension', + 'addSVGElementFromJson', + 'addToSelection', + 'alignSelectedElements', + 'assignAttributes', + 'bind', + 'call', + 'changeSelectedAttribute', + 'cleanupElement', + 'clear', + 'clearSelection', + 'clearSvgContentElement', + 'cloneLayer', + 'cloneSelectedElements', + 'convertGradients', + 'convertToGroup', + 'convertToNum', + 'convertToPath', + 'copySelectedElements', + 'createLayer', + 'cutSelectedElements', + 'cycleElement', + 'deleteCurrentLayer', + 'deleteSelectedElements', + 'embedImage', + 'exportPDF', + 'findDefs', + 'getBBox', + 'getBlur', + 'getBold', + 'getColor', + 'getContentElem', + 'getCurrentDrawing', + 'getDocumentTitle', + 'getEditorNS', + 'getElem', + 'getFillOpacity', + 'getFontColor', + 'getFontFamily', + 'getFontSize', + 'getHref', + 'getId', + 'getIntersectionList', + 'getItalic', + 'getMode', + 'getMouseTarget', + 'getNextId', + 'getOffset', + 'getOpacity', + 'getPaintOpacity', + 'getPrivateMethods', + 'getRefElem', + 'getResolution', + 'getRootElem', + 'getRotationAngle', + 'getSelectedElems', + 'getStrokeOpacity', + 'getStrokeWidth', + 'getStrokedBBox', + 'getStyle', + 'getSvgString', + 'getText', + 'getTitle', + 'getTransformList', + 'getUIStrings', + 'getUrlFromAttr', + 'getVersion', + 'getVisibleElements', + 'getVisibleElementsAndBBoxes', + 'getZoom', + 'groupSelectedElements', + 'groupSvgElem', + 'hasMatrixTransform', + 'identifyLayers', + 'importSvgString', + 'leaveContext', + 'linkControlPoints', + 'makeHyperlink', + 'matrixMultiply', + 'mergeAllLayers', + 'mergeLayer', + 'moveSelectedElements', + 'moveSelectedToLayer', + 'moveToBottomSelectedElement', + 'moveToTopSelectedElement', + 'moveUpDownSelected', + 'open', + 'pasteElements', + 'prepareSvg', + 'pushGroupProperties', + 'randomizeIds', + 'rasterExport', + 'ready', + 'recalculateAllSelectedDimensions', + 'recalculateDimensions', + 'remapElement', + 'removeFromSelection', + 'removeHyperlink', + 'removeUnusedDefElems', + 'renameCurrentLayer', + 'round', + 'runExtensions', + 'sanitizeSvg', + 'save', + 'selectAllInCurrentLayer', + 'selectOnly', + 'setBBoxZoom', + 'setBackground', + 'setBlur', + 'setBlurNoUndo', + 'setBlurOffsets', + 'setBold', + 'setColor', + 'setConfig', + 'setContext', + 'setCurrentLayer', + 'setCurrentLayerPosition', + 'setDocumentTitle', + 'setFillPaint', + 'setFontColor', + 'setFontFamily', + 'setFontSize', + 'setGoodImage', + 'setGradient', + 'setGroupTitle', + 'setHref', + 'setIdPrefix', + 'setImageURL', + 'setItalic', + 'setLayerVisibility', + 'setLinkURL', + 'setMode', + 'setOpacity', + 'setPaint', + 'setPaintOpacity', + 'setRectRadius', + 'setResolution', + 'setRotationAngle', + 'setSegType', + 'setStrokeAttr', + 'setStrokePaint', + 'setStrokeWidth', + 'setSvgString', + 'setTextContent', + 'setUiStrings', + 'setUseData', + 'setZoom', + 'svgCanvasToString', + 'svgToString', + 'transformListToTransform', + 'ungroupSelectedElement', + 'uniquifyElems', + 'updateCanvas', + 'zoomChanged' + ]; + + // TODO: rewrite the following, it's pretty scary. + for (let i = 0; i < functions.length; i++) { + this[functions[i]] = getCallbackSetter(functions[i]); + } + + // Older IE may need a polyfill for addEventListener, but so it would for SVG + window.addEventListener('message', getMessageListener(this), false); + window.addEventListener('keydown', (e) => { + const {key, keyCode, charCode, which} = e; + if (e.key === 'Backspace') { + e.preventDefault(); + const keyboardEvent = new KeyboardEvent(e.type, { + key, keyCode, charCode, which + }); + t.frame.contentDocument.dispatchEvent(keyboardEvent); + } + }); } - this.allowedOrigins = allowedOrigins || []; - // Initialize communication - this.frame = frame; - this.callbacks = {}; - // List of functions extracted with this: - // Run in firebug on http://svg-edit.googlecode.com/svn/trunk/docs/files/svgcanvas-js.html - // for (var i=0,q=[],f = document.querySelectorAll('div.CFunction h3.CTitle a'); i < f.length; i++) { q.push(f[i].name); }; q - // var functions = ['clearSelection', 'addToSelection', 'removeFromSelection', 'open', 'save', 'getSvgString', 'setSvgString', - // 'createLayer', 'deleteCurrentLayer', 'setCurrentLayer', 'renameCurrentLayer', 'setCurrentLayerPosition', 'setLayerVisibility', - // 'moveSelectedToLayer', 'clear']; + /** + * @param {string} name + * @param {ArgumentsArray} args Signature dependent on function + * @param {module:EmbeddedSVGEdit.GenericCallback} callback + */ + send (name, args, callback) { + const t = this; + cbid++; - // Newer, well, it extracts things that aren't documented as well. All functions accessible through the normal thingy can now be accessed though the API - // var svgCanvas = frame.contentWindow.svgCanvas; - // var l = []; for (var i in svgCanvas){ if (typeof svgCanvas[i] == 'function') { l.push(i);} }; - // alert("['" + l.join("', '") + "']"); - // Run in svgedit itself - var i, - functions = [ - 'clearSvgContentElement', 'setIdPrefix', 'getCurrentDrawing', 'addSvgElementFromJson', 'getTransformList', 'matrixMultiply', 'hasMatrixTransform', 'transformListToTransform', 'convertToNum', 'findDefs', 'getUrlFromAttr', 'getHref', 'setHref', 'getBBox', 'getRotationAngle', 'getElem', 'getRefElem', 'assignAttributes', 'cleanupElement', 'remapElement', 'recalculateDimensions', 'sanitizeSvg', 'runExtensions', 'addExtension', 'round', 'getIntersectionList', 'getStrokedBBox', 'getVisibleElements', 'getVisibleElementsAndBBoxes', 'groupSvgElem', 'getId', 'getNextId', 'call', 'bind', 'prepareSvg', 'setRotationAngle', 'recalculateAllSelectedDimensions', 'clearSelection', 'addToSelection', 'selectOnly', 'removeFromSelection', 'selectAllInCurrentLayer', 'getMouseTarget', 'removeUnusedDefElems', 'svgCanvasToString', 'svgToString', 'embedImage', 'setGoodImage', 'open', 'save', 'rasterExport', 'exportPDF', 'getSvgString', 'randomizeIds', 'uniquifyElems', 'setUseData', 'convertGradients', 'convertToGroup', 'setSvgString', 'importSvgString', 'identifyLayers', 'createLayer', 'cloneLayer', 'deleteCurrentLayer', 'setCurrentLayer', 'renameCurrentLayer', 'setCurrentLayerPosition', 'setLayerVisibility', 'moveSelectedToLayer', 'mergeLayer', 'mergeAllLayers', 'leaveContext', 'setContext', 'clear', 'linkControlPoints', 'getContentElem', 'getRootElem', 'getSelectedElems', 'getResolution', 'getZoom', 'getVersion', 'setUiStrings', 'setConfig', 'getTitle', 'setGroupTitle', 'getDocumentTitle', 'setDocumentTitle', 'getEditorNS', 'setResolution', 'getOffset', 'setBBoxZoom', 'setZoom', 'getMode', 'setMode', 'getColor', 'setColor', 'setGradient', 'setPaint', 'setStrokePaint', 'setFillPaint', 'getStrokeWidth', 'setStrokeWidth', 'setStrokeAttr', 'getStyle', 'getOpacity', 'setOpacity', 'getFillOpacity', 'getStrokeOpacity', 'setPaintOpacity', 'getPaintOpacity', 'getBlur', 'setBlurNoUndo', 'setBlurOffsets', 'setBlur', 'getBold', 'setBold', 'getItalic', 'setItalic', 'getFontFamily', 'setFontFamily', 'setFontColor', 'getFontColor', 'getFontSize', 'setFontSize', 'getText', 'setTextContent', 'setImageURL', 'setLinkURL', 'setRectRadius', 'makeHyperlink', 'removeHyperlink', 'setSegType', 'convertToPath', 'changeSelectedAttribute', 'deleteSelectedElements', 'cutSelectedElements', 'copySelectedElements', 'pasteElements', 'groupSelectedElements', 'pushGroupProperties', 'ungroupSelectedElement', 'moveToTopSelectedElement', 'moveToBottomSelectedElement', 'moveUpDownSelected', 'moveSelectedElements', 'cloneSelectedElements', 'alignSelectedElements', 'updateCanvas', 'setBackground', 'cycleElement', 'getPrivateMethods', 'zoomChanged', 'ready' - ]; + this.callbacks[cbid] = callback; + setTimeout((function (cbid) { + return function () { // Delay for the callback to be set in case its synchronous + /* + * Todo: Handle non-JSON arguments and return values (undefined, + * nonfinite numbers, functions, and built-in objects like Date, + * RegExp), etc.? Allow promises instead of callbacks? Review + * SVG-Edit functions for whether JSON-able parameters can be + * made compatile with all API functionality + */ + // We accept and post strings for the sake of IE9 support + let sameOrigin = false; + try { + sameOrigin = window.location.origin === t.frame.contentWindow.location.origin; + } catch (err) {} - // TODO: rewrite the following, it's pretty scary. - for (i = 0; i < functions.length; i++) { - this[functions[i]] = getCallbackSetter(functions[i]); + if (sameOrigin) { + // Although we do not really need this API if we are working same + // domain, it could allow us to write in a way that would work + // cross-domain as well, assuming we stick to the argument limitations + // of the current JSON-based communication API (e.g., not passing + // callbacks). We might be able to address these shortcomings; see + // the todo elsewhere in this file. + const message = {id: cbid}, + {svgEditor: {canvas: svgCanvas}} = t.frame.contentWindow; + try { + message.result = svgCanvas[name].apply(svgCanvas, args); + } catch (err) { + message.error = err.message; + } + addCallback(t, message); + } else { // Requires the ext-xdomain-messaging.js extension + t.frame.contentWindow.postMessage(JSON.stringify({ + namespace: 'svgCanvas', id: cbid, name, args + }), '*'); + } + }; + }(cbid)), 0); + + return cbid; } - - // Older IE may need a polyfill for addEventListener, but so it would for SVG - window.addEventListener('message', getMessageListener(this), false); } -EmbeddedSVGEdit.prototype.send = function (name, args, callback){ - var t = this; - cbid++; - - this.callbacks[cbid] = callback; - setTimeout((function (cbid) { - return function () { // Delay for the callback to be set in case its synchronous - /* - * Todo: Handle non-JSON arguments and return values (undefined, - * nonfinite numbers, functions, and built-in objects like Date, - * RegExp), etc.? Allow promises instead of callbacks? Review - * SVG-Edit functions for whether JSON-able parameters can be - * made compatile with all API functionality - */ - // We accept and post strings for the sake of IE9 support - if (window.location.origin === t.frame.contentWindow.location.origin) { - // Although we do not really need this API if we are working same - // domain, it could allow us to write in a way that would work - // cross-domain as well, assuming we stick to the argument limitations - // of the current JSON-based communication API (e.g., not passing - // callbacks). We might be able to address these shortcomings; see - // the todo elsewhere in this file. - var message = {id: cbid}, - svgCanvas = t.frame.contentWindow.svgCanvas; - try { - message.result = svgCanvas[name].apply(svgCanvas, args); - } - catch (err) { - message.error = err.message; - } - addCallback(t, message); - } - else { // Requires the ext-xdomain-messaging.js extension - t.frame.contentWindow.postMessage(JSON.stringify({namespace: 'svgCanvas', id: cbid, name: name, args: args}), '*'); - } - }; - }(cbid)), 0); - - return cbid; -}; - -window.embedded_svg_edit = EmbeddedSVGEdit; // Export old, deprecated API -window.EmbeddedSVGEdit = EmbeddedSVGEdit; // Follows common JS convention of CamelCase and, as enforced in JSLint, of initial caps for constructors - -}()); +export default EmbeddedSVGEdit; diff --git a/editor/extensions/allowedMimeTypes.php b/editor/extensions/allowedMimeTypes.php index 392f4d2d..2f99b159 100644 --- a/editor/extensions/allowedMimeTypes.php +++ b/editor/extensions/allowedMimeTypes.php @@ -1,12 +1,12 @@ 'image/svg+xml;charset=UTF-8', - 'png' => 'image/png', - 'jpeg' => 'image/jpeg', - 'bmp' => 'image/bmp', - 'webp' => 'image/webp', - 'pdf' => 'application/pdf' + 'svg' => 'image/svg+xml;charset=UTF-8', + 'png' => 'image/png', + 'jpeg' => 'image/jpeg', + 'bmp' => 'image/bmp', + 'webp' => 'image/webp', + 'pdf' => 'application/pdf' ); -?> \ No newline at end of file +?> diff --git a/editor/extensions/closepath.png b/editor/extensions/closepath.png new file mode 100644 index 00000000..7364bfc3 Binary files /dev/null and b/editor/extensions/closepath.png differ diff --git a/editor/extensions/ext-arrows.js b/editor/extensions/ext-arrows.js index 6bf9b3fe..1636dd23 100644 --- a/editor/extensions/ext-arrows.js +++ b/editor/extensions/ext-arrows.js @@ -1,293 +1,285 @@ -/*globals svgEditor, svgCanvas, $*/ -/*jslint vars: true, eqeq: true*/ -/* +/* globals jQuery */ +/** * ext-arrows.js * - * Licensed under the MIT License + * @license MIT * - * Copyright(c) 2010 Alexis Deveria + * @copyright 2010 Alexis Deveria * */ +export default { + name: 'arrows', + async init (S) { + const strings = await S.importLocale(); + const svgEditor = this; + const svgCanvas = svgEditor.canvas; + const $ = jQuery; + const // {svgcontent} = S, + addElem = svgCanvas.addSVGElementFromJson, + {nonce} = S, + prefix = 'se_arrow_'; -svgEditor.addExtension('Arrows', function(S) { - var svgcontent = S.svgcontent, - addElem = S.addSvgElementFromJson, - nonce = S.nonce, - randomize_ids = S.randomize_ids, - selElems, pathdata, - lang_list = { - 'en':[ - {'id': 'arrow_none', 'textContent': 'No arrow' } - ], - 'fr':[ - {'id': 'arrow_none', 'textContent': 'Sans flèche' } - ] - }, - arrowprefix, - prefix = 'se_arrow_'; + let selElems, arrowprefix, randomizeIds = S.randomize_ids; - function setArrowNonce(window, n) { - randomize_ids = true; - arrowprefix = prefix + n + '_'; - pathdata.fw.id = arrowprefix + 'fw'; - pathdata.bk.id = arrowprefix + 'bk'; - } + function setArrowNonce (window, n) { + randomizeIds = true; + arrowprefix = prefix + n + '_'; + pathdata.fw.id = arrowprefix + 'fw'; + pathdata.bk.id = arrowprefix + 'bk'; + } - function unsetArrowNonce(window) { - randomize_ids = false; - arrowprefix = prefix; - pathdata.fw.id = arrowprefix + 'fw'; - pathdata.bk.id = arrowprefix + 'bk'; - } + function unsetArrowNonce (window) { + randomizeIds = false; + arrowprefix = prefix; + pathdata.fw.id = arrowprefix + 'fw'; + pathdata.bk.id = arrowprefix + 'bk'; + } + svgCanvas.bind('setnonce', setArrowNonce); + svgCanvas.bind('unsetnonce', unsetArrowNonce); - svgCanvas.bind('setnonce', setArrowNonce); - svgCanvas.bind('unsetnonce', unsetArrowNonce); + if (randomizeIds) { + arrowprefix = prefix + nonce + '_'; + } else { + arrowprefix = prefix; + } - if (randomize_ids) { - arrowprefix = prefix + nonce + '_'; - } else { - arrowprefix = prefix; - } + const pathdata = { + fw: {d: 'm0,0l10,5l-10,5l5,-5l-5,-5z', refx: 8, id: arrowprefix + 'fw'}, + bk: {d: 'm10,0l-10,5l10,5l-5,-5l5,-5z', refx: 2, id: arrowprefix + 'bk'} + }; - pathdata = { - fw: {d: 'm0,0l10,5l-10,5l5,-5l-5,-5z', refx: 8, id: arrowprefix + 'fw'}, - bk: {d: 'm10,0l-10,5l10,5l-5,-5l5,-5z', refx: 2, id: arrowprefix + 'bk'} - }; + function getLinked (elem, attr) { + const str = elem.getAttribute(attr); + if (!str) { return null; } + const m = str.match(/\(#(.*)\)/); + if (!m || m.length !== 2) { + return null; + } + return svgCanvas.getElem(m[1]); + } - function getLinked(elem, attr) { - var str = elem.getAttribute(attr); - if(!str) {return null;} - var m = str.match(/\(\#(.*)\)/); - if(!m || m.length !== 2) { - return null; - } - return S.getElem(m[1]); - } + function showPanel (on) { + $('#arrow_panel').toggle(on); + if (on) { + const el = selElems[0]; + const end = el.getAttribute('marker-end'); + const start = el.getAttribute('marker-start'); + const mid = el.getAttribute('marker-mid'); + let val; + if (end && start) { + val = 'both'; + } else if (end) { + val = 'end'; + } else if (start) { + val = 'start'; + } else if (mid) { + val = 'mid'; + if (mid.includes('bk')) { + val = 'mid_bk'; + } + } - function showPanel(on) { - $('#arrow_panel').toggle(on); - if(on) { - var el = selElems[0]; - var end = el.getAttribute('marker-end'); - var start = el.getAttribute('marker-start'); - var mid = el.getAttribute('marker-mid'); - var val; + if (!start && !mid && !end) { + val = 'none'; + } - if (end && start) { - val = 'both'; - } else if (end) { - val = 'end'; - } else if (start) { - val = 'start'; - } else if (mid) { - val = 'mid'; - if (mid.indexOf('bk') !== -1) { - val = 'mid_bk'; - } - } + $('#arrow_list').val(val); + } + } - if (!start && !mid && !end) { - val = 'none'; - } + function resetMarker () { + const el = selElems[0]; + el.removeAttribute('marker-start'); + el.removeAttribute('marker-mid'); + el.removeAttribute('marker-end'); + } - $('#arrow_list').val(val); - } - } + function addMarker (dir, type, id) { + // TODO: Make marker (or use?) per arrow type, since refX can be different + id = id || arrowprefix + dir; - function resetMarker() { - var el = selElems[0]; - el.removeAttribute('marker-start'); - el.removeAttribute('marker-mid'); - el.removeAttribute('marker-end'); - } + const data = pathdata[dir]; - function addMarker(dir, type, id) { - // TODO: Make marker (or use?) per arrow type, since refX can be different - id = id || arrowprefix + dir; + if (type === 'mid') { + data.refx = 5; + } - var marker = S.getElem(id); - var data = pathdata[dir]; + let marker = svgCanvas.getElem(id); + if (!marker) { + marker = addElem({ + element: 'marker', + attr: { + viewBox: '0 0 10 10', + id, + refY: 5, + markerUnits: 'strokeWidth', + markerWidth: 5, + markerHeight: 5, + orient: 'auto', + style: 'pointer-events:none' // Currently needed for Opera + } + }); + const arrow = addElem({ + element: 'path', + attr: { + d: data.d, + fill: '#000000' + } + }); + marker.append(arrow); + svgCanvas.findDefs().append(marker); + } - if (type == 'mid') { - data.refx = 5; - } + marker.setAttribute('refX', data.refx); - if (!marker) { - marker = addElem({ - 'element': 'marker', - 'attr': { - 'viewBox': '0 0 10 10', - 'id': id, - 'refY': 5, - 'markerUnits': 'strokeWidth', - 'markerWidth': 5, - 'markerHeight': 5, - 'orient': 'auto', - 'style': 'pointer-events:none' // Currently needed for Opera - } - }); - var arrow = addElem({ - 'element': 'path', - 'attr': { - 'd': data.d, - 'fill': '#000000' - } - }); - marker.appendChild(arrow); - S.findDefs().appendChild(marker); - } + return marker; + } - marker.setAttribute('refX', data.refx); + function setArrow () { + resetMarker(); - return marker; - } + let type = this.value; + if (type === 'none') { + return; + } - function setArrow() { - var type = this.value; - resetMarker(); + // Set marker on element + let dir = 'fw'; + if (type === 'mid_bk') { + type = 'mid'; + dir = 'bk'; + } else if (type === 'both') { + addMarker('bk', type); + svgCanvas.changeSelectedAttribute('marker-start', 'url(#' + pathdata.bk.id + ')'); + type = 'end'; + dir = 'fw'; + } else if (type === 'start') { + dir = 'bk'; + } - if (type == 'none') { - return; - } + addMarker(dir, type); + svgCanvas.changeSelectedAttribute('marker-' + type, 'url(#' + pathdata[dir].id + ')'); + svgCanvas.call('changed', selElems); + } - // Set marker on element - var dir = 'fw'; - if (type == 'mid_bk') { - type = 'mid'; - dir = 'bk'; - } else if (type == 'both') { - addMarker('bk', type); - svgCanvas.changeSelectedAttribute('marker-start', 'url(#' + pathdata.bk.id + ')'); - type = 'end'; - dir = 'fw'; - } else if (type == 'start') { - dir = 'bk'; - } + function colorChanged (elem) { + const color = elem.getAttribute('stroke'); + const mtypes = ['start', 'mid', 'end']; + const defs = svgCanvas.findDefs(); - addMarker(dir, type); - svgCanvas.changeSelectedAttribute('marker-' + type, 'url(#' + pathdata[dir].id + ')'); - S.call('changed', selElems); - } + $.each(mtypes, function (i, type) { + const marker = getLinked(elem, 'marker-' + type); + if (!marker) { return; } - function colorChanged(elem) { - var color = elem.getAttribute('stroke'); - var mtypes = ['start', 'mid', 'end']; - var defs = S.findDefs(); + const curColor = $(marker).children().attr('fill'); + const curD = $(marker).children().attr('d'); + if (curColor === color) { return; } - $.each(mtypes, function(i, type) { - var marker = getLinked(elem, 'marker-'+type); - if(!marker) {return;} + const allMarkers = $(defs).find('marker'); + let newMarker = null; + // Different color, check if already made + allMarkers.each(function () { + const attrs = $(this).children().attr(['fill', 'd']); + if (attrs.fill === color && attrs.d === curD) { + // Found another marker with this color and this path + newMarker = this; + } + }); - var cur_color = $(marker).children().attr('fill'); - var cur_d = $(marker).children().attr('d'); - var new_marker = null; - if(cur_color === color) {return;} + if (!newMarker) { + // Create a new marker with this color + const lastId = marker.id; + const dir = lastId.includes('_fw') ? 'fw' : 'bk'; - var all_markers = $(defs).find('marker'); - // Different color, check if already made - all_markers.each(function() { - var attrs = $(this).children().attr(['fill', 'd']); - if(attrs.fill === color && attrs.d === cur_d) { - // Found another marker with this color and this path - new_marker = this; - } - }); + newMarker = addMarker(dir, type, arrowprefix + dir + allMarkers.length); - if(!new_marker) { - // Create a new marker with this color - var last_id = marker.id; - var dir = last_id.indexOf('_fw') !== -1?'fw':'bk'; + $(newMarker).children().attr('fill', color); + } - new_marker = addMarker(dir, type, arrowprefix + dir + all_markers.length); + $(elem).attr('marker-' + type, 'url(#' + newMarker.id + ')'); - $(new_marker).children().attr('fill', color); - } + // Check if last marker can be removed + let remove = true; + $(S.svgcontent).find('line, polyline, path, polygon').each(function () { + const elem = this; + $.each(mtypes, function (j, mtype) { + if ($(elem).attr('marker-' + mtype) === 'url(#' + marker.id + ')') { + remove = false; + return remove; + } + }); + if (!remove) { return false; } + }); - $(elem).attr('marker-'+type, 'url(#' + new_marker.id + ')'); + // Not found, so can safely remove + if (remove) { + $(marker).remove(); + } + }); + } - // Check if last marker can be removed - var remove = true; - $(S.svgcontent).find('line, polyline, path, polygon').each(function() { - var elem = this; - $.each(mtypes, function(j, mtype) { - if($(elem).attr('marker-' + mtype) === 'url(#' + marker.id + ')') { - remove = false; - return remove; - } - }); - if(!remove) {return false;} - }); + const contextTools = [ + { + type: 'select', + panel: 'arrow_panel', + id: 'arrow_list', + defval: 'none', + events: { + change: setArrow + } + } + ]; - // Not found, so can safely remove - if(remove) { - $(marker).remove(); - } - }); - } + return { + name: strings.name, + context_tools: strings.contextTools.map((contextTool, i) => { + return Object.assign(contextTools[i], contextTool); + }), + callback () { + $('#arrow_panel').hide(); + // Set ID so it can be translated in locale file + $('#arrow_list option')[0].id = 'connector_no_arrow'; + }, + async addLangData ({lang, importLocale}) { + const strings = await importLocale(); + return { + data: strings.langList + }; + }, + selectedChanged (opts) { + // Use this to update the current selected elements + selElems = opts.elems; - return { - name: 'Arrows', - context_tools: [{ - type: 'select', - panel: 'arrow_panel', - title: 'Select arrow type', - id: 'arrow_list', - options: { - none: 'No arrow', - end: '---->', - start: '<----', - both: '<--->', - mid: '-->--', - mid_bk: '--<--' - }, - defval: 'none', - events: { - change: setArrow - } - }], - callback: function() { - $('#arrow_panel').hide(); - // Set ID so it can be translated in locale file - $('#arrow_list option')[0].id = 'connector_no_arrow'; - }, - addLangData: function(lang) { - return { - data: lang_list[lang] - }; - }, - selectedChanged: function(opts) { - // Use this to update the current selected elements - selElems = opts.elems; - - var i = selElems.length; - var marker_elems = ['line', 'path', 'polyline', 'polygon']; - while(i--) { - var elem = selElems[i]; - if(elem && $.inArray(elem.tagName, marker_elems) !== -1) { - if(opts.selectedElement && !opts.multiselected) { - showPanel(true); - } else { - showPanel(false); - } - } else { - showPanel(false); - } - } - }, - elementChanged: function(opts) { - var elem = opts.elems[0]; - if(elem && ( - elem.getAttribute('marker-start') || - elem.getAttribute('marker-mid') || - elem.getAttribute('marker-end') - )) { -// var start = elem.getAttribute('marker-start'); -// var mid = elem.getAttribute('marker-mid'); -// var end = elem.getAttribute('marker-end'); - // Has marker, so see if it should match color - colorChanged(elem); - } - } - }; -}); + const markerElems = ['line', 'path', 'polyline', 'polygon']; + let i = selElems.length; + while (i--) { + const elem = selElems[i]; + if (elem && markerElems.includes(elem.tagName)) { + if (opts.selectedElement && !opts.multiselected) { + showPanel(true); + } else { + showPanel(false); + } + } else { + showPanel(false); + } + } + }, + elementChanged (opts) { + const elem = opts.elems[0]; + if (elem && ( + elem.getAttribute('marker-start') || + elem.getAttribute('marker-mid') || + elem.getAttribute('marker-end') + )) { + // const start = elem.getAttribute('marker-start'); + // const mid = elem.getAttribute('marker-mid'); + // const end = elem.getAttribute('marker-end'); + // Has marker, so see if it should match color + colorChanged(elem); + } + } + }; + } +}; diff --git a/editor/extensions/ext-closepath.js b/editor/extensions/ext-closepath.js index b7858424..9406e361 100644 --- a/editor/extensions/ext-closepath.js +++ b/editor/extensions/ext-closepath.js @@ -1,91 +1,103 @@ -/*globals svgEditor, $*/ -/*jslint vars: true, eqeq: true*/ -/* +/* globals jQuery */ +/** * ext-closepath.js * - * Licensed under the MIT License + * @license MIT * - * Copyright(c) 2010 Jeff Schiller + * @copyright 2010 Jeff Schiller * */ +import '../svgpathseg.js'; // This extension adds a simple button to the contextual panel for paths // The button toggles whether the path is open or closed -svgEditor.addExtension('ClosePath', function() {'use strict'; - var selElems, - updateButton = function(path) { - var seglist = path.pathSegList, - closed = seglist.getItem(seglist.numberOfItems - 1).pathSegType == 1, - showbutton = closed ? '#tool_openpath' : '#tool_closepath', - hidebutton = closed ? '#tool_closepath' : '#tool_openpath'; - $(hidebutton).hide(); - $(showbutton).show(); - }, - showPanel = function(on) { - $('#closepath_panel').toggle(on); - if (on) { - var path = selElems[0]; - if (path) {updateButton(path);} - } - }, - toggleClosed = function() { - var path = selElems[0]; - if (path) { - var seglist = path.pathSegList, - last = seglist.numberOfItems - 1; - // is closed - if (seglist.getItem(last).pathSegType == 1) { - seglist.removeItem(last); - } else { - seglist.appendItem(path.createSVGPathSegClosePath()); - } - updateButton(path); - } - }; +export default { + name: 'closepath', + async init ({importLocale}) { + const strings = await importLocale(); + const $ = jQuery; + const svgEditor = this; + let selElems; + const updateButton = function (path) { + const seglist = path.pathSegList, + closed = seglist.getItem(seglist.numberOfItems - 1).pathSegType === 1, + showbutton = closed ? '#tool_openpath' : '#tool_closepath', + hidebutton = closed ? '#tool_closepath' : '#tool_openpath'; + $(hidebutton).hide(); + $(showbutton).show(); + }; + const showPanel = function (on) { + $('#closepath_panel').toggle(on); + if (on) { + const path = selElems[0]; + if (path) { updateButton(path); } + } + }; + const toggleClosed = function () { + const path = selElems[0]; + if (path) { + const seglist = path.pathSegList, + last = seglist.numberOfItems - 1; + // is closed + if (seglist.getItem(last).pathSegType === 1) { + seglist.removeItem(last); + } else { + seglist.appendItem(path.createSVGPathSegClosePath()); + } + updateButton(path); + } + }; - return { - name: 'ClosePath', - svgicons: svgEditor.curConfig.extPath + 'closepath_icons.svg', - buttons: [{ - id: 'tool_openpath', - type: 'context', - panel: 'closepath_panel', - title: 'Open path', - events: { - click: function() { - toggleClosed(); - } - } - }, - { - id: 'tool_closepath', - type: 'context', - panel: 'closepath_panel', - title: 'Close path', - events: { - click: function() { - toggleClosed(); - } - } - }], - callback: function() { - $('#closepath_panel').hide(); - }, - selectedChanged: function(opts) { - selElems = opts.elems; - var i = selElems.length; - while (i--) { - var elem = selElems[i]; - if (elem && elem.tagName == 'path') { - if (opts.selectedElement && !opts.multiselected) { - showPanel(true); - } else { - showPanel(false); - } - } else { - showPanel(false); - } - } - } - }; -}); + const buttons = [ + { + id: 'tool_openpath', + icon: svgEditor.curConfig.extIconsPath + 'openpath.png', + type: 'context', + panel: 'closepath_panel', + events: { + click () { + toggleClosed(); + } + } + }, + { + id: 'tool_closepath', + icon: svgEditor.curConfig.extIconsPath + 'closepath.png', + type: 'context', + panel: 'closepath_panel', + events: { + click () { + toggleClosed(); + } + } + } + ]; + + return { + name: strings.name, + svgicons: svgEditor.curConfig.extIconsPath + 'closepath_icons.svg', + buttons: strings.buttons.map((button, i) => { + return Object.assign(buttons[i], button); + }), + callback () { + $('#closepath_panel').hide(); + }, + selectedChanged (opts) { + selElems = opts.elems; + let i = selElems.length; + while (i--) { + const elem = selElems[i]; + if (elem && elem.tagName === 'path') { + if (opts.selectedElement && !opts.multiselected) { + showPanel(true); + } else { + showPanel(false); + } + } else { + showPanel(false); + } + } + } + }; + } +}; diff --git a/editor/extensions/ext-connector.js b/editor/extensions/ext-connector.js index 9c6e631c..e5e13df6 100644 --- a/editor/extensions/ext-connector.js +++ b/editor/extensions/ext-connector.js @@ -1,620 +1,616 @@ -/*globals svgEditor, svgCanvas, $*/ -/*jslint vars: true, continue: true, eqeq: true, todo: true*/ -/* +/* globals jQuery */ +/** * ext-connector.js * - * Licensed under the MIT License + * @license MIT * - * Copyright(c) 2010 Alexis Deveria + * @copyright 2010 Alexis Deveria * */ - -svgEditor.addExtension("Connector", function(S) { - var svgcontent = S.svgcontent, - svgroot = S.svgroot, - getNextId = S.getNextId, - getElem = S.getElem, - addElem = S.addSvgElementFromJson, - selManager = S.selectorManager, - curConfig = svgEditor.curConfig, - started = false, - start_x, - start_y, - cur_line, - start_elem, - end_elem, - connections = [], - conn_sel = ".se_connector", - se_ns, -// connect_str = "-SE_CONNECT-", - selElems = [], - elData = $.data; - - var lang_list = { - "en":[ - {"id": "mode_connect", "title": "Connect two objects" } - ], - "fr":[ - {"id": "mode_connect", "title": "Connecter deux objets"} - ] - }; - function getBBintersect(x, y, bb, offset) { - if(offset) { - offset -= 0; - bb = $.extend({}, bb); - bb.width += offset; - bb.height += offset; - bb.x -= offset/2; - bb.y -= offset/2; - } - - var mid_x = bb.x + bb.width/2; - var mid_y = bb.y + bb.height/2; - var len_x = x - mid_x; - var len_y = y - mid_y; - - var slope = Math.abs(len_y/len_x); - - var ratio; - - if(slope < bb.height/bb.width) { - ratio = (bb.width/2) / Math.abs(len_x); - } else { - ratio = (bb.height/2) / Math.abs(len_y); - } - - - return { - x: mid_x + len_x * ratio, - y: mid_y + len_y * ratio - }; - } +export default { + name: 'connector', + async init (S) { + const $ = jQuery; + const svgEditor = this; + const svgCanvas = svgEditor.canvas; + const {getElem} = svgCanvas; + const {svgroot, importLocale} = S, + addElem = svgCanvas.addSVGElementFromJson, + selManager = S.selectorManager, + connSel = '.se_connector', + // connect_str = '-SE_CONNECT-', + elData = $.data; + const strings = await importLocale(); - function getOffset(side, line) { - var give_offset = !!line.getAttribute('marker-' + side); -// var give_offset = $(line).data(side+'_off'); + let startX, + startY, + curLine, + startElem, + endElem, + seNs, + {svgcontent} = S, + started = false, + connections = [], + selElems = []; - // TODO: Make this number (5) be based on marker width/height - var size = line.getAttribute('stroke-width') * 5; - return give_offset ? size : 0; - } - - function showPanel(on) { - var conn_rules = $('#connector_rules'); - if(!conn_rules.length) { - conn_rules = $('').appendTo('head'); - } - conn_rules.text(!on?"":"#tool_clone, #tool_topath, #tool_angle, #xy_panel { display: none !important; }"); - $('#connector_panel').toggle(on); - } - - function setPoint(elem, pos, x, y, setMid) { - var i, pts = elem.points; - var pt = svgroot.createSVGPoint(); - pt.x = x; - pt.y = y; - if (pos === 'end') {pos = pts.numberOfItems - 1;} - // TODO: Test for this on init, then use alt only if needed - try { - pts.replaceItem(pt, pos); - } catch(err) { - // Should only occur in FF which formats points attr as "n,n n,n", so just split - var pt_arr = elem.getAttribute("points").split(" "); - for (i = 0; i < pt_arr.length; i++) { - if (i === pos) { - pt_arr[i] = x + ',' + y; - } - } - elem.setAttribute("points",pt_arr.join(" ")); - } - - if(setMid) { - // Add center point - var pt_start = pts.getItem(0); - var pt_end = pts.getItem(pts.numberOfItems-1); - setPoint(elem, 1, (pt_end.x + pt_start.x)/2, (pt_end.y + pt_start.y)/2); - } - } - - function updateLine(diff_x, diff_y) { - // Update line with element - var i = connections.length; - while(i--) { - var conn = connections[i]; - var line = conn.connector; - var elem = conn.elem; - - var pre = conn.is_start?'start':'end'; -// var sw = line.getAttribute('stroke-width') * 5; - - // Update bbox for this element - var bb = elData(line, pre+'_bb'); - bb.x = conn.start_x + diff_x; - bb.y = conn.start_y + diff_y; - elData(line, pre+'_bb', bb); - - var alt_pre = conn.is_start?'end':'start'; - - // Get center pt of connected element - var bb2 = elData(line, alt_pre+'_bb'); - var src_x = bb2.x + bb2.width/2; - var src_y = bb2.y + bb2.height/2; - - // Set point of element being moved - var pt = getBBintersect(src_x, src_y, bb, getOffset(pre, line)); // $(line).data(pre+'_off')?sw:0 - setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true); - - // Set point of connected element - var pt2 = getBBintersect(pt.x, pt.y, elData(line, alt_pre + '_bb'), getOffset(alt_pre, line)); - setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true); + function getBBintersect (x, y, bb, offset) { + if (offset) { + offset -= 0; + bb = $.extend({}, bb); + bb.width += offset; + bb.height += offset; + bb.x -= offset / 2; + bb.y -= offset / 2; + } - } - } - - function findConnectors(elems) { - var i; - if (!elems) {elems = selElems;} - var connectors = $(svgcontent).find(conn_sel); - connections = []; + const midX = bb.x + bb.width / 2; + const midY = bb.y + bb.height / 2; + const lenX = x - midX; + const lenY = y - midY; - // Loop through connectors to see if one is connected to the element - connectors.each(function() { - var add_this; - function add () { - if ($.inArray(this, elems) !== -1) { - // Pretend this element is selected - add_this = true; - } - } + const slope = Math.abs(lenY / lenX); - // Grab the ends - var parts = []; - ['start', 'end'].forEach(function (pos, i) { - var key = 'c_' + pos; - var part = elData(this, key); - if(part == null) { - part = document.getElementById( - this.attributes['se:connector'].value.split(' ')[i] - ); - elData(this, 'c_'+pos, part.id); - elData(this, pos+'_bb', svgCanvas.getStrokedBBox([part])); - } else part = document.getElementById(part); - parts.push(part); - }.bind(this)); + let ratio; + if (slope < bb.height / bb.width) { + ratio = (bb.width / 2) / Math.abs(lenX); + } else { + ratio = lenY + ? (bb.height / 2) / Math.abs(lenY) + : 0; + } - for (i = 0; i < 2; i++) { - var c_elem = parts[i]; + return { + x: midX + lenX * ratio, + y: midY + lenY * ratio + }; + } - add_this = false; - // The connected element might be part of a selected group - $(c_elem).parents().each(add); - - if(!c_elem || !c_elem.parentNode) { - $(this).remove(); - continue; - } - if($.inArray(c_elem, elems) !== -1 || add_this) { - var bb = svgCanvas.getStrokedBBox([c_elem]); - connections.push({ - elem: c_elem, - connector: this, - is_start: (i === 0), - start_x: bb.x, - start_y: bb.y - }); - } - } - }); - } - - function updateConnectors(elems) { - // Updates connector lines based on selected elements - // Is not used on mousemove, as it runs getStrokedBBox every time, - // which isn't necessary there. - var i, j; - findConnectors(elems); - if (connections.length) { - // Update line with element - i = connections.length; - while (i--) { - var conn = connections[i]; - var line = conn.connector; - var elem = conn.elem; + function getOffset (side, line) { + const giveOffset = !!line.getAttribute('marker-' + side); + // const giveOffset = $(line).data(side+'_off'); - var sw = line.getAttribute('stroke-width') * 5; - var pre = conn.is_start?'start':'end'; - - // Update bbox for this element - var bb = svgCanvas.getStrokedBBox([elem]); - bb.x = conn.start_x; - bb.y = conn.start_y; - elData(line, pre+'_bb', bb); - var add_offset = elData(line, pre+'_off'); - - var alt_pre = conn.is_start?'end':'start'; - - // Get center pt of connected element - var bb2 = elData(line, alt_pre+'_bb'); - var src_x = bb2.x + bb2.width/2; - var src_y = bb2.y + bb2.height/2; - - // Set point of element being moved - var pt = getBBintersect(src_x, src_y, bb, getOffset(pre, line)); - setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true); - - // Set point of connected element - var pt2 = getBBintersect(pt.x, pt.y, elData(line, alt_pre + '_bb'), getOffset(alt_pre, line)); - setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true); - - // Update points attribute manually for webkit - if (navigator.userAgent.indexOf('AppleWebKit') !== -1) { - var pts = line.points; - var len = pts.numberOfItems; - var pt_arr = []; - for (j = 0; j < len; j++) { - pt = pts.getItem(j); - pt_arr[j] = pt.x + ',' + pt.y; - } - line.setAttribute('points', pt_arr.join(' ')); - } - } - } - } - - // Do once - (function() { - var gse = svgCanvas.groupSelectedElements; - - svgCanvas.groupSelectedElements = function() { - svgCanvas.removeFromSelection($(conn_sel).toArray()); - return gse.apply(this, arguments); - }; - - var mse = svgCanvas.moveSelectedElements; - - svgCanvas.moveSelectedElements = function() { - var cmd = mse.apply(this, arguments); - updateConnectors(); - return cmd; - }; - - se_ns = svgCanvas.getEditorNS(); - }()); - - // Do on reset - function init() { - // Make sure all connectors have data set - $(svgcontent).find('*').each(function() { - var conn = this.getAttributeNS(se_ns, "connector"); - if(conn) { - this.setAttribute('class', conn_sel.substr(1)); - var conn_data = conn.split(' '); - var sbb = svgCanvas.getStrokedBBox([getElem(conn_data[0])]); - var ebb = svgCanvas.getStrokedBBox([getElem(conn_data[1])]); - $(this).data('c_start',conn_data[0]) - .data('c_end',conn_data[1]) - .data('start_bb', sbb) - .data('end_bb', ebb); - svgCanvas.getEditorNS(true); - } - }); -// updateConnectors(); - } + // TODO: Make this number (5) be based on marker width/height + const size = line.getAttribute('stroke-width') * 5; + return giveOffset ? size : 0; + } -// $(svgroot).parent().mousemove(function(e) { -// // if(started -// // || svgCanvas.getMode() != "connector" -// // || e.target.parentNode.parentNode != svgcontent) return; -// -// console.log('y') -// // if(e.target.parentNode.parentNode === svgcontent) { -// // -// // } -// }); + function showPanel (on) { + let connRules = $('#connector_rules'); + if (!connRules.length) { + connRules = $('').appendTo('head'); + } + connRules.text(!on ? '' : '#tool_clone, #tool_topath, #tool_angle, #xy_panel { display: none !important; }'); + $('#connector_panel').toggle(on); + } - return { - name: "Connector", - svgicons: svgEditor.curConfig.imgPath + "conn.svg", - buttons: [{ - id: "mode_connect", - type: "mode", - icon: svgEditor.curConfig.imgPath + "cut.png", - title: "Connect two objects", - includeWith: { - button: '#tool_line', - isDefault: false, - position: 1 - }, - events: { - 'click': function() { - svgCanvas.setMode("connector"); - } - } - }], - addLangData: function(lang) { - return { - data: lang_list[lang] - }; - }, - mouseDown: function(opts) { - var e = opts.event; - start_x = opts.start_x; - start_y = opts.start_y; - var mode = svgCanvas.getMode(); - - if (mode == "connector") { - - if (started) {return;} + function setPoint (elem, pos, x, y, setMid) { + const pts = elem.points; + const pt = svgroot.createSVGPoint(); + pt.x = x; + pt.y = y; + if (pos === 'end') { pos = pts.numberOfItems - 1; } + // TODO: Test for this on init, then use alt only if needed + try { + pts.replaceItem(pt, pos); + } catch (err) { + // Should only occur in FF which formats points attr as "n,n n,n", so just split + const ptArr = elem.getAttribute('points').split(' '); + for (let i = 0; i < ptArr.length; i++) { + if (i === pos) { + ptArr[i] = x + ',' + y; + } + } + elem.setAttribute('points', ptArr.join(' ')); + } - var mouse_target = e.target; - - var parents = $(mouse_target).parents(); - - if($.inArray(svgcontent, parents) !== -1) { - // Connectable element - - // If child of foreignObject, use parent - var fo = $(mouse_target).closest("foreignObject"); - start_elem = fo.length ? fo[0] : mouse_target; - - // Get center of source element - var bb = svgCanvas.getStrokedBBox([start_elem]); - var x = bb.x + bb.width/2; - var y = bb.y + bb.height/2; - - started = true; - cur_line = addElem({ - "element": "polyline", - "attr": { - "id": getNextId(), - "points": (x+','+y+' '+x+','+y+' '+start_x+','+start_y), - "stroke": '#' + curConfig.initStroke.color, - "stroke-width": (!start_elem.stroke_width || start_elem.stroke_width == 0) ? curConfig.initStroke.width : start_elem.stroke_width, - "fill": "none", - "opacity": curConfig.initStroke.opacity, - "style": "pointer-events:none" - } - }); - elData(cur_line, 'start_bb', bb); - } - return { - started: true - }; - } - if (mode == "select") { - findConnectors(); - } - }, - mouseMove: function(opts) { - var zoom = svgCanvas.getZoom(); - var e = opts.event; - var x = opts.mouse_x/zoom; - var y = opts.mouse_y/zoom; - - var diff_x = x - start_x, - diff_y = y - start_y; - - var mode = svgCanvas.getMode(); - - if (mode == "connector" && started) { - - var sw = cur_line.getAttribute('stroke-width') * 3; - // Set start point (adjusts based on bb) - var pt = getBBintersect(x, y, elData(cur_line, 'start_bb'), getOffset('start', cur_line)); - start_x = pt.x; - start_y = pt.y; - - setPoint(cur_line, 0, pt.x, pt.y, true); - - // Set end point - setPoint(cur_line, 'end', x, y, true); - } else if (mode == "select") { - var slen = selElems.length; - - while(slen--) { - var elem = selElems[slen]; - // Look for selected connector elements - if(elem && elData(elem, 'c_start')) { - // Remove the "translate" transform given to move - svgCanvas.removeFromSelection([elem]); - svgCanvas.getTransformList(elem).clear(); + if (setMid) { + // Add center point + const ptStart = pts.getItem(0); + const ptEnd = pts.getItem(pts.numberOfItems - 1); + setPoint(elem, 1, (ptEnd.x + ptStart.x) / 2, (ptEnd.y + ptStart.y) / 2); + } + } - } - } - if(connections.length) { - updateLine(diff_x, diff_y); + function updateLine (diffX, diffY) { + // Update line with element + let i = connections.length; + while (i--) { + const conn = connections[i]; + const line = conn.connector; + // const {elem} = conn; - - } - } - }, - mouseUp: function(opts) { - var zoom = svgCanvas.getZoom(); - var e = opts.event, - x = opts.mouse_x/zoom, - y = opts.mouse_y/zoom, - mouse_target = e.target; - - if(svgCanvas.getMode() == "connector") { - var fo = $(mouse_target).closest("foreignObject"); - if (fo.length) {mouse_target = fo[0];} - - var parents = $(mouse_target).parents(); + const pre = conn.is_start ? 'start' : 'end'; + // const sw = line.getAttribute('stroke-width') * 5; - if (mouse_target == start_elem) { - // Start line through click - started = true; - return { - keep: true, - element: null, - started: started - }; - } - if ($.inArray(svgcontent, parents) === -1) { - // Not a valid target element, so remove line - $(cur_line).remove(); - started = false; - return { - keep: false, - element: null, - started: started - }; - } - // Valid end element - end_elem = mouse_target; - - var start_id = start_elem.id, end_id = end_elem.id; - var conn_str = start_id + " " + end_id; - var alt_str = end_id + " " + start_id; - // Don't create connector if one already exists - var dupe = $(svgcontent).find(conn_sel).filter(function() { - var conn = this.getAttributeNS(se_ns, "connector"); - if (conn == conn_str || conn == alt_str) {return true;} - }); - if(dupe.length) { - $(cur_line).remove(); - return { - keep: false, - element: null, - started: false - }; - } - - var bb = svgCanvas.getStrokedBBox([end_elem]); - - var pt = getBBintersect(start_x, start_y, bb, getOffset('start', cur_line)); - setPoint(cur_line, 'end', pt.x, pt.y, true); - $(cur_line) - .data("c_start", start_id) - .data("c_end", end_id) - .data("end_bb", bb); - se_ns = svgCanvas.getEditorNS(true); - cur_line.setAttributeNS(se_ns, "se:connector", conn_str); - cur_line.setAttribute('class', conn_sel.substr(1)); - cur_line.setAttribute('opacity', 1); - svgCanvas.addToSelection([cur_line]); - svgCanvas.moveToBottomSelectedElement(); - selManager.requestSelector(cur_line).showGrips(false); - started = false; - return { - keep: true, - element: cur_line, - started: started - }; - } - }, - selectedChanged: function(opts) { - // TODO: Find better way to skip operations if no connectors are in use - if(!$(svgcontent).find(conn_sel).length) {return;} - - if(svgCanvas.getMode() == 'connector') { - svgCanvas.setMode('select'); - } - - // Use this to update the current selected elements - selElems = opts.elems; - - var i = selElems.length; - - while(i--) { - var elem = selElems[i]; - if(elem && elData(elem, 'c_start')) { - selManager.requestSelector(elem).showGrips(false); - if(opts.selectedElement && !opts.multiselected) { - // TODO: Set up context tools and hide most regular line tools - showPanel(true); - } else { - showPanel(false); - } - } else { - showPanel(false); - } - } - updateConnectors(); - }, - elementChanged: function(opts) { - var elem = opts.elems[0]; - if (elem && elem.tagName === 'svg' && elem.id === 'svgcontent') { - // Update svgcontent (can change on import) - svgcontent = elem; - init(); - } - - // Has marker, so change offset - var start; - if (elem && ( - elem.getAttribute("marker-start") || - elem.getAttribute("marker-mid") || - elem.getAttribute("marker-end") - )) { - start = elem.getAttribute("marker-start"); - var mid = elem.getAttribute("marker-mid"); - var end = elem.getAttribute("marker-end"); - cur_line = elem; - $(elem) - .data("start_off", !!start) - .data("end_off", !!end); - - if (elem.tagName === 'line' && mid) { - // Convert to polyline to accept mid-arrow - - var x1 = Number(elem.getAttribute('x1')); - var x2 = Number(elem.getAttribute('x2')); - var y1 = Number(elem.getAttribute('y1')); - var y2 = Number(elem.getAttribute('y2')); - var id = elem.id; - - var mid_pt = (' '+((x1+x2)/2)+','+((y1+y2)/2) + ' '); - var pline = addElem({ - "element": "polyline", - "attr": { - "points": (x1+','+y1+ mid_pt +x2+','+y2), - "stroke": elem.getAttribute('stroke'), - "stroke-width": elem.getAttribute('stroke-width'), - "marker-mid": mid, - "fill": "none", - "opacity": elem.getAttribute('opacity') || 1 - } - }); - $(elem).after(pline).remove(); - svgCanvas.clearSelection(); - pline.id = id; - svgCanvas.addToSelection([pline]); - elem = pline; - } - } - // Update line if it's a connector - if (elem.getAttribute('class') == conn_sel.substr(1)) { - start = getElem(elData(elem, 'c_start')); - updateConnectors([start]); - } else { - updateConnectors(); - } - }, - IDsUpdated: function(input) { - var remove = []; - input.elems.forEach(function(elem){ - if('se:connector' in elem.attr) { - elem.attr['se:connector'] = elem.attr['se:connector'].split(' ') - .map(function(oldID){ return input.changes[oldID] }).join(' '); + // Update bbox for this element + const bb = elData(line, pre + '_bb'); + bb.x = conn.start_x + diffX; + bb.y = conn.start_y + diffY; + elData(line, pre + '_bb', bb); - // Check validity - the field would be something like 'svg_21 svg_22', but - // if one end is missing, it would be 'svg_21' and therefore fail this test - if(!/. ./.test(elem.attr['se:connector'])) - remove.push(elem.attr.id); - } - }); - return {remove: remove}; - }, - toolButtonStateUpdate: function(opts) { - if (opts.nostroke) { - if ($('#mode_connect').hasClass('tool_button_current')) { - svgEditor.clickSelect(); - } - } - $('#mode_connect') - .toggleClass('disabled',opts.nostroke); - } - }; -}); + const altPre = conn.is_start ? 'end' : 'start'; + + // Get center pt of connected element + const bb2 = elData(line, altPre + '_bb'); + const srcX = bb2.x + bb2.width / 2; + const srcY = bb2.y + bb2.height / 2; + + // Set point of element being moved + const pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); // $(line).data(pre+'_off')?sw:0 + setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true); + + // Set point of connected element + const pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line)); + setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true); + } + } + + /** + * + * @param {Element[]} [elem=selElems] Array of elements + */ + function findConnectors (elems = selElems) { + const connectors = $(svgcontent).find(connSel); + connections = []; + + // Loop through connectors to see if one is connected to the element + connectors.each(function () { + let addThis; + function add () { + if (elems.includes(this)) { + // Pretend this element is selected + addThis = true; + } + } + + // Grab the ends + const parts = []; + ['start', 'end'].forEach(function (pos, i) { + const key = 'c_' + pos; + let part = elData(this, key); + if (part == null) { + part = document.getElementById( + this.attributes['se:connector'].value.split(' ')[i] + ); + elData(this, 'c_' + pos, part.id); + elData(this, pos + '_bb', svgCanvas.getStrokedBBox([part])); + } else part = document.getElementById(part); + parts.push(part); + }.bind(this)); + + for (let i = 0; i < 2; i++) { + const cElem = parts[i]; + + addThis = false; + // The connected element might be part of a selected group + $(cElem).parents().each(add); + + if (!cElem || !cElem.parentNode) { + $(this).remove(); + continue; + } + if (elems.includes(cElem) || addThis) { + const bb = svgCanvas.getStrokedBBox([cElem]); + connections.push({ + elem: cElem, + connector: this, + is_start: (i === 0), + start_x: bb.x, + start_y: bb.y + }); + } + } + }); + } + + function updateConnectors (elems) { + // Updates connector lines based on selected elements + // Is not used on mousemove, as it runs getStrokedBBox every time, + // which isn't necessary there. + findConnectors(elems); + if (connections.length) { + // Update line with element + let i = connections.length; + while (i--) { + const conn = connections[i]; + const line = conn.connector; + const {elem} = conn; + + // const sw = line.getAttribute('stroke-width') * 5; + const pre = conn.is_start ? 'start' : 'end'; + + // Update bbox for this element + const bb = svgCanvas.getStrokedBBox([elem]); + bb.x = conn.start_x; + bb.y = conn.start_y; + elData(line, pre + '_bb', bb); + /* const addOffset = */ elData(line, pre + '_off'); + + const altPre = conn.is_start ? 'end' : 'start'; + + // Get center pt of connected element + const bb2 = elData(line, altPre + '_bb'); + const srcX = bb2.x + bb2.width / 2; + const srcY = bb2.y + bb2.height / 2; + + // Set point of element being moved + let pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); + setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true); + + // Set point of connected element + const pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line)); + setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true); + + // Update points attribute manually for webkit + if (navigator.userAgent.includes('AppleWebKit')) { + const pts = line.points; + const len = pts.numberOfItems; + const ptArr = []; + for (let j = 0; j < len; j++) { + pt = pts.getItem(j); + ptArr[j] = pt.x + ',' + pt.y; + } + line.setAttribute('points', ptArr.join(' ')); + } + } + } + } + + // Do once + (function () { + const gse = svgCanvas.groupSelectedElements; + + svgCanvas.groupSelectedElements = function () { + svgCanvas.removeFromSelection($(connSel).toArray()); + return gse.apply(this, arguments); + }; + + const mse = svgCanvas.moveSelectedElements; + + svgCanvas.moveSelectedElements = function () { + const cmd = mse.apply(this, arguments); + updateConnectors(); + return cmd; + }; + + seNs = svgCanvas.getEditorNS(); + }()); + + // Do on reset + function init () { + // Make sure all connectors have data set + $(svgcontent).find('*').each(function () { + const conn = this.getAttributeNS(seNs, 'connector'); + if (conn) { + this.setAttribute('class', connSel.substr(1)); + const connData = conn.split(' '); + const sbb = svgCanvas.getStrokedBBox([getElem(connData[0])]); + const ebb = svgCanvas.getStrokedBBox([getElem(connData[1])]); + $(this).data('c_start', connData[0]) + .data('c_end', connData[1]) + .data('start_bb', sbb) + .data('end_bb', ebb); + svgCanvas.getEditorNS(true); + } + }); + // updateConnectors(); + } + + // $(svgroot).parent().mousemove(function (e) { + // // if (started + // // || svgCanvas.getMode() !== 'connector' + // // || e.target.parentNode.parentNode !== svgcontent) return; + // + // console.log('y') + // // if (e.target.parentNode.parentNode === svgcontent) { + // // + // // } + // }); + + const buttons = [{ + id: 'mode_connect', + type: 'mode', + icon: svgEditor.curConfig.imgPath + 'cut.png', + includeWith: { + button: '#tool_line', + isDefault: false, + position: 1 + }, + events: { + click () { + svgCanvas.setMode('connector'); + } + } + }]; + + return { + name: strings.name, + svgicons: svgEditor.curConfig.imgPath + 'conn.svg', + buttons: strings.buttons.map((button, i) => { + return Object.assign(buttons[i], button); + }), + async addLangData ({lang, importLocale}) { + return { + data: strings.langList + }; + }, + mouseDown (opts) { + const e = opts.event; + startX = opts.start_x; + startY = opts.start_y; + const mode = svgCanvas.getMode(); + const {curConfig: {initStroke}} = svgEditor; + + if (mode === 'connector') { + if (started) { return; } + + const mouseTarget = e.target; + + const parents = $(mouseTarget).parents(); + + if ($.inArray(svgcontent, parents) !== -1) { + // Connectable element + + // If child of foreignObject, use parent + const fo = $(mouseTarget).closest('foreignObject'); + startElem = fo.length ? fo[0] : mouseTarget; + + // Get center of source element + const bb = svgCanvas.getStrokedBBox([startElem]); + const x = bb.x + bb.width / 2; + const y = bb.y + bb.height / 2; + + started = true; + curLine = addElem({ + element: 'polyline', + attr: { + id: svgCanvas.getNextId(), + points: (x + ',' + y + ' ' + x + ',' + y + ' ' + startX + ',' + startY), + stroke: '#' + initStroke.color, + 'stroke-width': (!startElem.stroke_width || startElem.stroke_width === 0) + ? initStroke.width + : startElem.stroke_width, + fill: 'none', + opacity: initStroke.opacity, + style: 'pointer-events:none' + } + }); + elData(curLine, 'start_bb', bb); + } + return { + started: true + }; + } + if (mode === 'select') { + findConnectors(); + } + }, + mouseMove (opts) { + const zoom = svgCanvas.getZoom(); + // const e = opts.event; + const x = opts.mouse_x / zoom; + const y = opts.mouse_y / zoom; + + const diffX = x - startX, + diffY = y - startY; + + const mode = svgCanvas.getMode(); + + if (mode === 'connector' && started) { + // const sw = curLine.getAttribute('stroke-width') * 3; + // Set start point (adjusts based on bb) + const pt = getBBintersect(x, y, elData(curLine, 'start_bb'), getOffset('start', curLine)); + startX = pt.x; + startY = pt.y; + + setPoint(curLine, 0, pt.x, pt.y, true); + + // Set end point + setPoint(curLine, 'end', x, y, true); + } else if (mode === 'select') { + let slen = selElems.length; + while (slen--) { + const elem = selElems[slen]; + // Look for selected connector elements + if (elem && elData(elem, 'c_start')) { + // Remove the "translate" transform given to move + svgCanvas.removeFromSelection([elem]); + svgCanvas.getTransformList(elem).clear(); + } + } + if (connections.length) { + updateLine(diffX, diffY); + } + } + }, + mouseUp (opts) { + // const zoom = svgCanvas.getZoom(); + const e = opts.event; + // , x = opts.mouse_x / zoom, + // , y = opts.mouse_y / zoom, + let mouseTarget = e.target; + + if (svgCanvas.getMode() !== 'connector') { + return; + } + const fo = $(mouseTarget).closest('foreignObject'); + if (fo.length) { mouseTarget = fo[0]; } + + const parents = $(mouseTarget).parents(); + + if (mouseTarget === startElem) { + // Start line through click + started = true; + return { + keep: true, + element: null, + started + }; + } + if ($.inArray(svgcontent, parents) === -1) { + // Not a valid target element, so remove line + $(curLine).remove(); + started = false; + return { + keep: false, + element: null, + started + }; + } + // Valid end element + endElem = mouseTarget; + + const startId = startElem.id, endId = endElem.id; + const connStr = startId + ' ' + endId; + const altStr = endId + ' ' + startId; + // Don't create connector if one already exists + const dupe = $(svgcontent).find(connSel).filter(function () { + const conn = this.getAttributeNS(seNs, 'connector'); + if (conn === connStr || conn === altStr) { return true; } + }); + if (dupe.length) { + $(curLine).remove(); + return { + keep: false, + element: null, + started: false + }; + } + + const bb = svgCanvas.getStrokedBBox([endElem]); + + const pt = getBBintersect(startX, startY, bb, getOffset('start', curLine)); + setPoint(curLine, 'end', pt.x, pt.y, true); + $(curLine) + .data('c_start', startId) + .data('c_end', endId) + .data('end_bb', bb); + seNs = svgCanvas.getEditorNS(true); + curLine.setAttributeNS(seNs, 'se:connector', connStr); + curLine.setAttribute('class', connSel.substr(1)); + curLine.setAttribute('opacity', 1); + svgCanvas.addToSelection([curLine]); + svgCanvas.moveToBottomSelectedElement(); + selManager.requestSelector(curLine).showGrips(false); + started = false; + return { + keep: true, + element: curLine, + started + }; + }, + selectedChanged (opts) { + // TODO: Find better way to skip operations if no connectors are in use + if (!$(svgcontent).find(connSel).length) { return; } + + if (svgCanvas.getMode() === 'connector') { + svgCanvas.setMode('select'); + } + + // Use this to update the current selected elements + selElems = opts.elems; + + let i = selElems.length; + while (i--) { + const elem = selElems[i]; + if (elem && elData(elem, 'c_start')) { + selManager.requestSelector(elem).showGrips(false); + if (opts.selectedElement && !opts.multiselected) { + // TODO: Set up context tools and hide most regular line tools + showPanel(true); + } else { + showPanel(false); + } + } else { + showPanel(false); + } + } + updateConnectors(); + }, + elementChanged (opts) { + let elem = opts.elems[0]; + if (elem && elem.tagName === 'svg' && elem.id === 'svgcontent') { + // Update svgcontent (can change on import) + svgcontent = elem; + init(); + } + + // Has marker, so change offset + if (elem && ( + elem.getAttribute('marker-start') || + elem.getAttribute('marker-mid') || + elem.getAttribute('marker-end') + )) { + const start = elem.getAttribute('marker-start'); + const mid = elem.getAttribute('marker-mid'); + const end = elem.getAttribute('marker-end'); + curLine = elem; + $(elem) + .data('start_off', !!start) + .data('end_off', !!end); + + if (elem.tagName === 'line' && mid) { + // Convert to polyline to accept mid-arrow + + const x1 = Number(elem.getAttribute('x1')); + const x2 = Number(elem.getAttribute('x2')); + const y1 = Number(elem.getAttribute('y1')); + const y2 = Number(elem.getAttribute('y2')); + const {id} = elem; + + const midPt = (' ' + ((x1 + x2) / 2) + ',' + ((y1 + y2) / 2) + ' '); + const pline = addElem({ + element: 'polyline', + attr: { + points: (x1 + ',' + y1 + midPt + x2 + ',' + y2), + stroke: elem.getAttribute('stroke'), + 'stroke-width': elem.getAttribute('stroke-width'), + 'marker-mid': mid, + fill: 'none', + opacity: elem.getAttribute('opacity') || 1 + } + }); + $(elem).after(pline).remove(); + svgCanvas.clearSelection(); + pline.id = id; + svgCanvas.addToSelection([pline]); + elem = pline; + } + } + // Update line if it's a connector + if (elem.getAttribute('class') === connSel.substr(1)) { + const start = getElem(elData(elem, 'c_start')); + updateConnectors([start]); + } else { + updateConnectors(); + } + }, + IDsUpdated (input) { + const remove = []; + input.elems.forEach(function (elem) { + if ('se:connector' in elem.attr) { + elem.attr['se:connector'] = elem.attr['se:connector'].split(' ') + .map(function (oldID) { return input.changes[oldID]; }).join(' '); + + // Check validity - the field would be something like 'svg_21 svg_22', but + // if one end is missing, it would be 'svg_21' and therefore fail this test + if (!/. ./.test(elem.attr['se:connector'])) { + remove.push(elem.attr.id); + } + } + }); + return {remove}; + }, + toolButtonStateUpdate (opts) { + if (opts.nostroke) { + if ($('#mode_connect').hasClass('tool_button_current')) { + svgEditor.clickSelect(); + } + } + $('#mode_connect') + .toggleClass('disabled', opts.nostroke); + } + }; + } +}; diff --git a/editor/extensions/ext-eyedropper.js b/editor/extensions/ext-eyedropper.js index cc4961ca..3acaa298 100644 --- a/editor/extensions/ext-eyedropper.js +++ b/editor/extensions/ext-eyedropper.js @@ -1,112 +1,112 @@ -/*globals svgEditor, svgedit, $*/ -/*jslint vars: true, eqeq: true*/ -/* +/* globals jQuery */ +/** * ext-eyedropper.js * - * Licensed under the MIT License + * @license MIT * - * Copyright(c) 2010 Jeff Schiller + * @copyright 2010 Jeff Schiller * */ -// Dependencies: -// 1) jQuery -// 2) history.js -// 3) svg_editor.js -// 4) svgcanvas.js +export default { + name: 'eyedropper', + async init (S) { + const strings = await S.importLocale(); + const svgEditor = this; + const $ = jQuery; + const {ChangeElementCommand} = S, // , svgcontent, + // svgdoc = S.svgroot.parentNode.ownerDocument, + svgCanvas = svgEditor.canvas, + addToHistory = function (cmd) { svgCanvas.undoMgr.addCommandToHistory(cmd); }, + currentStyle = { + fillPaint: 'red', fillOpacity: 1.0, + strokePaint: 'black', strokeOpacity: 1.0, + strokeWidth: 5, strokeDashArray: null, + opacity: 1.0, + strokeLinecap: 'butt', + strokeLinejoin: 'miter' + }; -svgEditor.addExtension("eyedropper", function(S) {'use strict'; - var // NS = svgedit.NS, - // svgcontent = S.svgcontent, - // svgdoc = S.svgroot.parentNode.ownerDocument, - svgCanvas = svgEditor.canvas, - ChangeElementCommand = svgedit.history.ChangeElementCommand, - addToHistory = function(cmd) { svgCanvas.undoMgr.addCommandToHistory(cmd); }, - currentStyle = { - fillPaint: "red", fillOpacity: 1.0, - strokePaint: "black", strokeOpacity: 1.0, - strokeWidth: 5, strokeDashArray: null, - opacity: 1.0, - strokeLinecap: 'butt', - strokeLinejoin: 'miter' - }; + function getStyle (opts) { + // if we are in eyedropper mode, we don't want to disable the eye-dropper tool + const mode = svgCanvas.getMode(); + if (mode === 'eyedropper') { return; } - function getStyle(opts) { - // if we are in eyedropper mode, we don't want to disable the eye-dropper tool - var mode = svgCanvas.getMode(); - if (mode == "eyedropper") {return;} + const tool = $('#tool_eyedropper'); + // enable-eye-dropper if one element is selected + let elem = null; + if (!opts.multiselected && opts.elems[0] && + !['svg', 'g', 'use'].includes(opts.elems[0].nodeName) + ) { + elem = opts.elems[0]; + tool.removeClass('disabled'); + // grab the current style + currentStyle.fillPaint = elem.getAttribute('fill') || 'black'; + currentStyle.fillOpacity = elem.getAttribute('fill-opacity') || 1.0; + currentStyle.strokePaint = elem.getAttribute('stroke'); + currentStyle.strokeOpacity = elem.getAttribute('stroke-opacity') || 1.0; + currentStyle.strokeWidth = elem.getAttribute('stroke-width'); + currentStyle.strokeDashArray = elem.getAttribute('stroke-dasharray'); + currentStyle.strokeLinecap = elem.getAttribute('stroke-linecap'); + currentStyle.strokeLinejoin = elem.getAttribute('stroke-linejoin'); + currentStyle.opacity = elem.getAttribute('opacity') || 1.0; + // disable eye-dropper tool + } else { + tool.addClass('disabled'); + } + } - var elem = null; - var tool = $('#tool_eyedropper'); - // enable-eye-dropper if one element is selected - if (!opts.multiselected && opts.elems[0] && - $.inArray(opts.elems[0].nodeName, ['svg', 'g', 'use']) === -1 - ) { - elem = opts.elems[0]; - tool.removeClass('disabled'); - // grab the current style - currentStyle.fillPaint = elem.getAttribute("fill") || "black"; - currentStyle.fillOpacity = elem.getAttribute("fill-opacity") || 1.0; - currentStyle.strokePaint = elem.getAttribute("stroke"); - currentStyle.strokeOpacity = elem.getAttribute("stroke-opacity") || 1.0; - currentStyle.strokeWidth = elem.getAttribute("stroke-width"); - currentStyle.strokeDashArray = elem.getAttribute("stroke-dasharray"); - currentStyle.strokeLinecap = elem.getAttribute("stroke-linecap"); - currentStyle.strokeLinejoin = elem.getAttribute("stroke-linejoin"); - currentStyle.opacity = elem.getAttribute("opacity") || 1.0; - } - // disable eye-dropper tool - else { - tool.addClass('disabled'); - } + const buttons = [ + { + id: 'tool_eyedropper', + icon: svgEditor.curConfig.extIconsPath + 'eyedropper.png', + type: 'mode', + events: { + click () { + svgCanvas.setMode('eyedropper'); + } + } + } + ]; - } - - return { - name: "eyedropper", - svgicons: svgEditor.curConfig.extPath + "eyedropper-icon.xml", - buttons: [{ - id: "tool_eyedropper", - type: "mode", - title: "Eye Dropper Tool", - key: "I", - events: { - "click": function() { - svgCanvas.setMode("eyedropper"); - } - } - }], - - // if we have selected an element, grab its paint and enable the eye dropper button - selectedChanged: getStyle, - elementChanged: getStyle, - - mouseDown: function(opts) { - var mode = svgCanvas.getMode(); - if (mode == "eyedropper") { - var e = opts.event; - var target = e.target; - if ($.inArray(target.nodeName, ['svg', 'g', 'use']) === -1) { - var changes = {}; + return { + name: strings.name, + svgicons: svgEditor.curConfig.extIconsPath + 'eyedropper-icon.xml', + buttons: strings.buttons.map((button, i) => { + return Object.assign(buttons[i], button); + }), - var change = function(elem, attrname, newvalue) { - changes[attrname] = elem.getAttribute(attrname); - elem.setAttribute(attrname, newvalue); - }; - - if (currentStyle.fillPaint) {change(target, "fill", currentStyle.fillPaint);} - if (currentStyle.fillOpacity) {change(target, "fill-opacity", currentStyle.fillOpacity);} - if (currentStyle.strokePaint) {change(target, "stroke", currentStyle.strokePaint);} - if (currentStyle.strokeOpacity) {change(target, "stroke-opacity", currentStyle.strokeOpacity);} - if (currentStyle.strokeWidth) {change(target, "stroke-width", currentStyle.strokeWidth);} - if (currentStyle.strokeDashArray) {change(target, "stroke-dasharray", currentStyle.strokeDashArray);} - if (currentStyle.opacity) {change(target, "opacity", currentStyle.opacity);} - if (currentStyle.strokeLinecap) {change(target, "stroke-linecap", currentStyle.strokeLinecap);} - if (currentStyle.strokeLinejoin) {change(target, "stroke-linejoin", currentStyle.strokeLinejoin);} - - addToHistory(new ChangeElementCommand(target, changes)); - } - } - } - }; -}); + // if we have selected an element, grab its paint and enable the eye dropper button + selectedChanged: getStyle, + elementChanged: getStyle, + + mouseDown (opts) { + const mode = svgCanvas.getMode(); + if (mode === 'eyedropper') { + const e = opts.event; + const {target} = e; + if (!['svg', 'g', 'use'].includes(target.nodeName)) { + const changes = {}; + + const change = function (elem, attrname, newvalue) { + changes[attrname] = elem.getAttribute(attrname); + elem.setAttribute(attrname, newvalue); + }; + + if (currentStyle.fillPaint) { change(target, 'fill', currentStyle.fillPaint); } + if (currentStyle.fillOpacity) { change(target, 'fill-opacity', currentStyle.fillOpacity); } + if (currentStyle.strokePaint) { change(target, 'stroke', currentStyle.strokePaint); } + if (currentStyle.strokeOpacity) { change(target, 'stroke-opacity', currentStyle.strokeOpacity); } + if (currentStyle.strokeWidth) { change(target, 'stroke-width', currentStyle.strokeWidth); } + if (currentStyle.strokeDashArray) { change(target, 'stroke-dasharray', currentStyle.strokeDashArray); } + if (currentStyle.opacity) { change(target, 'opacity', currentStyle.opacity); } + if (currentStyle.strokeLinecap) { change(target, 'stroke-linecap', currentStyle.strokeLinecap); } + if (currentStyle.strokeLinejoin) { change(target, 'stroke-linejoin', currentStyle.strokeLinejoin); } + + addToHistory(new ChangeElementCommand(target, changes)); + } + } + } + }; + } +}; diff --git a/editor/extensions/ext-foreignobject.js b/editor/extensions/ext-foreignobject.js index 136485a9..78420564 100644 --- a/editor/extensions/ext-foreignobject.js +++ b/editor/extensions/ext-foreignobject.js @@ -1,268 +1,265 @@ -/*globals svgEditor, svgedit, svgCanvas, $*/ -/*jslint vars: true, eqeq: true, todo: true*/ -/* +/* globals jQuery */ +/** * ext-foreignobject.js * - * Licensed under the Apache License, Version 2 + * @license Apache-2.0 * - * Copyright(c) 2010 Jacques Distler - * Copyright(c) 2010 Alexis Deveria + * @copyright 2010 Jacques Distler, 2010 Alexis Deveria * */ -svgEditor.addExtension("foreignObject", function(S) { - var NS = svgedit.NS, - Utils = svgedit.utilities, - svgcontent = S.svgcontent, - addElem = S.addSvgElementFromJson, - selElems, - editingforeign = false, - svgdoc = S.svgroot.parentNode.ownerDocument, - started, - newFO; +export default { + name: 'foreignobject', + async init (S) { + const svgEditor = this; + const {text2xml, NS, importLocale} = S; + const $ = jQuery; + const svgCanvas = svgEditor.canvas; + const + // {svgcontent} = S, + // addElem = svgCanvas.addSVGElementFromJson, + svgdoc = S.svgroot.parentNode.ownerDocument; + const strings = await importLocale(); - var properlySourceSizeTextArea = function () { - // TODO: remove magic numbers here and get values from CSS - var height = $('#svg_source_container').height() - 80; - $('#svg_source_textarea').css('height', height); - }; + const properlySourceSizeTextArea = function () { + // TODO: remove magic numbers here and get values from CSS + const height = $('#svg_source_container').height() - 80; + $('#svg_source_textarea').css('height', height); + }; - function showPanel(on) { - var fc_rules = $('#fc_rules'); - if(!fc_rules.length) { - fc_rules = $('').appendTo('head'); - } - fc_rules.text(!on?"":" #tool_topath { display: none !important; }"); - $('#foreignObject_panel').toggle(on); - } + function showPanel (on) { + let fcRules = $('#fc_rules'); + if (!fcRules.length) { + fcRules = $('').appendTo('head'); + } + fcRules.text(!on ? '' : ' #tool_topath { display: none !important; }'); + $('#foreignObject_panel').toggle(on); + } - function toggleSourceButtons(on) { - $('#tool_source_save, #tool_source_cancel').toggle(!on); - $('#foreign_save, #foreign_cancel').toggle(on); - } + function toggleSourceButtons (on) { + $('#tool_source_save, #tool_source_cancel').toggle(!on); + $('#foreign_save, #foreign_cancel').toggle(on); + } - // Function: setForeignString(xmlString, elt) - // This function sets the content of element elt to the input XML. - // - // Parameters: - // xmlString - The XML text. - // elt - the parent element to append to - // - // Returns: - // This function returns false if the set was unsuccessful, true otherwise. - function setForeignString(xmlString) { - var elt = selElems[0]; - try { - // convert string into XML document - var newDoc = Utils.text2xml('' + xmlString + ''); - // run it through our sanitizer to remove anything we do not support - S.sanitizeSvg(newDoc.documentElement); - elt.parentNode.replaceChild(svgdoc.importNode(newDoc.documentElement.firstChild, true), elt); - S.call("changed", [elt]); - svgCanvas.clearSelection(); - } catch(e) { - console.log(e); - return false; - } + let selElems, + started, + newFO, + editingforeign = false; - return true; - } + /** + * This function sets the content of element elt to the input XML. + * @param {string} xmlString - The XML text + * @param {Element} elt - the parent element to append to + * @returns {boolean} This function returns false if the set was unsuccessful, true otherwise. + */ + function setForeignString (xmlString) { + const elt = selElems[0]; + try { + // convert string into XML document + const newDoc = text2xml('' + xmlString + ''); + // run it through our sanitizer to remove anything we do not support + svgCanvas.sanitizeSvg(newDoc.documentElement); + elt.replaceWith(svgdoc.importNode(newDoc.documentElement.firstChild, true)); + svgCanvas.call('changed', [elt]); + svgCanvas.clearSelection(); + } catch (e) { + console.log(e); + return false; + } - function showForeignEditor() { - var elt = selElems[0]; - if (!elt || editingforeign) {return;} - editingforeign = true; - toggleSourceButtons(true); - elt.removeAttribute('fill'); + return true; + } - var str = S.svgToString(elt, 0); - $('#svg_source_textarea').val(str); - $('#svg_source_editor').fadeIn(); - properlySourceSizeTextArea(); - $('#svg_source_textarea').focus(); - } + function showForeignEditor () { + const elt = selElems[0]; + if (!elt || editingforeign) { return; } + editingforeign = true; + toggleSourceButtons(true); + elt.removeAttribute('fill'); - function setAttr(attr, val) { - svgCanvas.changeSelectedAttribute(attr, val); - S.call("changed", selElems); - } + const str = svgCanvas.svgToString(elt, 0); + $('#svg_source_textarea').val(str); + $('#svg_source_editor').fadeIn(); + properlySourceSizeTextArea(); + $('#svg_source_textarea').focus(); + } - return { - name: "foreignObject", - svgicons: svgEditor.curConfig.extPath + "foreignobject-icons.xml", - buttons: [{ - id: "tool_foreign", - type: "mode", - title: "Foreign Object Tool", - events: { - 'click': function() { - svgCanvas.setMode('foreign'); - } - } - },{ - id: "edit_foreign", - type: "context", - panel: "foreignObject_panel", - title: "Edit ForeignObject Content", - events: { - 'click': function() { - showForeignEditor(); - } - } - }], + function setAttr (attr, val) { + svgCanvas.changeSelectedAttribute(attr, val); + svgCanvas.call('changed', selElems); + } - context_tools: [{ - type: "input", - panel: "foreignObject_panel", - title: "Change foreignObject's width", - id: "foreign_width", - label: "w", - size: 3, - events: { - change: function() { - setAttr('width', this.value); - } - } - },{ - type: "input", - panel: "foreignObject_panel", - title: "Change foreignObject's height", - id: "foreign_height", - label: "h", - events: { - change: function() { - setAttr('height', this.value); - } - } - }, { - type: "input", - panel: "foreignObject_panel", - title: "Change foreignObject's font size", - id: "foreign_font_size", - label: "font-size", - size: 2, - defval: 16, - events: { - change: function() { - setAttr('font-size', this.value); - } - } - } + const buttons = [{ + id: 'tool_foreign', + icon: svgEditor.curConfig.extIconsPath + 'foreignobject-tool.png', + type: 'mode', + events: { + click () { + svgCanvas.setMode('foreign'); + } + } + }, { + id: 'edit_foreign', + icon: svgEditor.curConfig.extIconsPath + 'foreignobject-edit.png', + type: 'context', + panel: 'foreignObject_panel', + events: { + click () { + showForeignEditor(); + } + } + }]; - ], - callback: function() { - $('#foreignObject_panel').hide(); + const contextTools = [ + { + type: 'input', + panel: 'foreignObject_panel', + id: 'foreign_width', + size: 3, + events: { + change () { + setAttr('width', this.value); + } + } + }, { + type: 'input', + panel: 'foreignObject_panel', + id: 'foreign_height', + events: { + change () { + setAttr('height', this.value); + } + } + }, { + type: 'input', + panel: 'foreignObject_panel', + id: 'foreign_font_size', + size: 2, + defval: 16, + events: { + change () { + setAttr('font-size', this.value); + } + } + } + ]; - var endChanges = function() { - $('#svg_source_editor').hide(); - editingforeign = false; - $('#svg_source_textarea').blur(); - toggleSourceButtons(false); - }; + return { + name: strings.name, + svgicons: svgEditor.curConfig.extIconsPath + 'foreignobject-icons.xml', + buttons: strings.buttons.map((button, i) => { + return Object.assign(buttons[i], button); + }), + context_tools: strings.contextTools.map((contextTool, i) => { + return Object.assign(contextTools[i], contextTool); + }), + callback () { + $('#foreignObject_panel').hide(); - // TODO: Needs to be done after orig icon loads - setTimeout(function() { - // Create source save/cancel buttons - var save = $('#tool_source_save').clone() - .hide().attr('id', 'foreign_save').unbind() - .appendTo("#tool_source_back").click(function() { + const endChanges = function () { + $('#svg_source_editor').hide(); + editingforeign = false; + $('#svg_source_textarea').blur(); + toggleSourceButtons(false); + }; - if (!editingforeign) {return;} + // TODO: Needs to be done after orig icon loads + setTimeout(function () { + // Create source save/cancel buttons + /* const save = */ $('#tool_source_save').clone() + .hide().attr('id', 'foreign_save').unbind() + .appendTo('#tool_source_back').click(function () { + if (!editingforeign) { return; } - if (!setForeignString($('#svg_source_textarea').val())) { - $.confirm("Errors found. Revert to original?", function(ok) { - if(!ok) {return false;} - endChanges(); - }); - } else { - endChanges(); - } - // setSelectMode(); - }); + if (!setForeignString($('#svg_source_textarea').val())) { + $.confirm('Errors found. Revert to original?', function (ok) { + if (!ok) { return false; } + endChanges(); + }); + } else { + endChanges(); + } + // setSelectMode(); + }); - var cancel = $('#tool_source_cancel').clone() - .hide().attr('id', 'foreign_cancel').unbind() - .appendTo("#tool_source_back").click(function() { - endChanges(); - }); - }, 3000); - }, - mouseDown: function(opts) { - var e = opts.event; + /* const cancel = */ $('#tool_source_cancel').clone() + .hide().attr('id', 'foreign_cancel').unbind() + .appendTo('#tool_source_back').click(function () { + endChanges(); + }); + }, 3000); + }, + mouseDown (opts) { + // const e = opts.event; - if(svgCanvas.getMode() == "foreign") { + if (svgCanvas.getMode() === 'foreign') { + started = true; + newFO = svgCanvas.addSVGElementFromJson({ + element: 'foreignObject', + attr: { + x: opts.start_x, + y: opts.start_y, + id: svgCanvas.getNextId(), + 'font-size': 16, // cur_text.font_size, + width: '48', + height: '20', + style: 'pointer-events:inherit' + } + }); + const m = svgdoc.createElementNS(NS.MATH, 'math'); + m.setAttributeNS(NS.XMLNS, 'xmlns', NS.MATH); + m.setAttribute('display', 'inline'); + const mi = svgdoc.createElementNS(NS.MATH, 'mi'); + mi.setAttribute('mathvariant', 'normal'); + mi.textContent = '\u03A6'; + const mo = svgdoc.createElementNS(NS.MATH, 'mo'); + mo.textContent = '\u222A'; + const mi2 = svgdoc.createElementNS(NS.MATH, 'mi'); + mi2.textContent = '\u2133'; + m.append(mi, mo, mi2); + newFO.append(m); + return { + started: true + }; + } + }, + mouseUp (opts) { + // const e = opts.event; + if (svgCanvas.getMode() === 'foreign' && started) { + const attrs = $(newFO).attr(['width', 'height']); + const keep = (attrs.width !== '0' || attrs.height !== '0'); + svgCanvas.addToSelection([newFO], true); - started = true; - newFO = S.addSvgElementFromJson({ - "element": "foreignObject", - "attr": { - "x": opts.start_x, - "y": opts.start_y, - "id": S.getNextId(), - "font-size": 16, //cur_text.font_size, - "width": "48", - "height": "20", - "style": "pointer-events:inherit" - } - }); - var m = svgdoc.createElementNS(NS.MATH, 'math'); - m.setAttributeNS(NS.XMLNS, 'xmlns', NS.MATH); - m.setAttribute('display', 'inline'); - var mi = svgdoc.createElementNS(NS.MATH, 'mi'); - mi.setAttribute('mathvariant', 'normal'); - mi.textContent = "\u03A6"; - var mo = svgdoc.createElementNS(NS.MATH, 'mo'); - mo.textContent = "\u222A"; - var mi2 = svgdoc.createElementNS(NS.MATH, 'mi'); - mi2.textContent = "\u2133"; - m.appendChild(mi); - m.appendChild(mo); - m.appendChild(mi2); - newFO.appendChild(m); - return { - started: true - }; - } - }, - mouseUp: function(opts) { - var e = opts.event; - if(svgCanvas.getMode() == "foreign" && started) { - var attrs = $(newFO).attr(["width", "height"]); - var keep = (attrs.width != 0 || attrs.height != 0); - svgCanvas.addToSelection([newFO], true); + return { + keep, + element: newFO + }; + } + }, + selectedChanged (opts) { + // Use this to update the current selected elements + selElems = opts.elems; - return { - keep: keep, - element: newFO - }; - - } - - }, - selectedChanged: function(opts) { - // Use this to update the current selected elements - selElems = opts.elems; - - var i = selElems.length; - - while(i--) { - var elem = selElems[i]; - if(elem && elem.tagName === 'foreignObject') { - if(opts.selectedElement && !opts.multiselected) { - $('#foreign_font_size').val(elem.getAttribute("font-size")); - $('#foreign_width').val(elem.getAttribute("width")); - $('#foreign_height').val(elem.getAttribute("height")); - showPanel(true); - } else { - showPanel(false); - } - } else { - showPanel(false); - } - } - }, - elementChanged: function(opts) { - var elem = opts.elems[0]; - } - }; -}); + let i = selElems.length; + while (i--) { + const elem = selElems[i]; + if (elem && elem.tagName === 'foreignObject') { + if (opts.selectedElement && !opts.multiselected) { + $('#foreign_font_size').val(elem.getAttribute('font-size')); + $('#foreign_width').val(elem.getAttribute('width')); + $('#foreign_height').val(elem.getAttribute('height')); + showPanel(true); + } else { + showPanel(false); + } + } else { + showPanel(false); + } + } + }, + elementChanged (opts) { + // const elem = opts.elems[0]; + } + }; + } +}; diff --git a/editor/extensions/ext-grid.js b/editor/extensions/ext-grid.js index a4148da7..831c46bb 100644 --- a/editor/extensions/ext-grid.js +++ b/editor/extensions/ext-grid.js @@ -1,162 +1,163 @@ -/*globals svgEditor, svgedit, svgCanvas, $*/ -/*jslint vars: true*/ -/* +/* globals jQuery */ +/** * ext-grid.js * - * Licensed under the Apache License, Version 2 + * @license Apache-2.0 * - * Copyright(c) 2010 Redou Mine - * Copyright(c) 2010 Alexis Deveria + * @copyright 2010 Redou Mine, 2010 Alexis Deveria * */ -// Dependencies: -// 1) units.js -// 2) everything else +export default { + name: 'grid', + async init ({NS, getTypeMap, importLocale}) { + const strings = await importLocale(); + const svgEditor = this; + const $ = jQuery; + const svgCanvas = svgEditor.canvas; + const svgdoc = document.getElementById('svgcanvas').ownerDocument, + {assignAttributes} = svgCanvas, + hcanvas = document.createElement('canvas'), + canvBG = $('#canvasBackground'), + units = getTypeMap(), // Assumes prior `init()` call on `units.js` module + intervals = [0.01, 0.1, 1, 10, 100, 1000]; + let showGrid = svgEditor.curConfig.showGrid || false; -svgEditor.addExtension('view_grid', function() { 'use strict'; + $(hcanvas).hide().appendTo('body'); - var NS = svgedit.NS, - svgdoc = document.getElementById('svgcanvas').ownerDocument, - showGrid = svgEditor.curConfig.showGrid || false, - assignAttributes = svgCanvas.assignAttributes, - hcanvas = document.createElement('canvas'), - canvBG = $('#canvasBackground'), - units = svgedit.units.getTypeMap(), - intervals = [0.01, 0.1, 1, 10, 100, 1000]; + const canvasGrid = svgdoc.createElementNS(NS.SVG, 'svg'); + assignAttributes(canvasGrid, { + id: 'canvasGrid', + width: '100%', + height: '100%', + x: 0, + y: 0, + overflow: 'visible', + display: 'none' + }); + canvBG.append(canvasGrid); - $(hcanvas).hide().appendTo('body'); + // grid-pattern + const gridPattern = svgdoc.createElementNS(NS.SVG, 'pattern'); + assignAttributes(gridPattern, { + id: 'gridpattern', + patternUnits: 'userSpaceOnUse', + x: 0, // -(value.strokeWidth / 2), // position for strokewidth + y: 0, // -(value.strokeWidth / 2), // position for strokewidth + width: 100, + height: 100 + }); - var canvasGrid = svgdoc.createElementNS(NS.SVG, 'svg'); - assignAttributes(canvasGrid, { - 'id': 'canvasGrid', - 'width': '100%', - 'height': '100%', - 'x': 0, - 'y': 0, - 'overflow': 'visible', - 'display': 'none' - }); - canvBG.append(canvasGrid); + const gridimg = svgdoc.createElementNS(NS.SVG, 'image'); + assignAttributes(gridimg, { + x: 0, + y: 0, + width: 100, + height: 100 + }); + gridPattern.append(gridimg); + $('#svgroot defs').append(gridPattern); - // grid-pattern - var gridPattern = svgdoc.createElementNS(NS.SVG, 'pattern'); - assignAttributes(gridPattern, { - 'id': 'gridpattern', - 'patternUnits': 'userSpaceOnUse', - 'x': 0, //-(value.strokeWidth / 2), // position for strokewidth - 'y': 0, //-(value.strokeWidth / 2), // position for strokewidth - 'width': 100, - 'height': 100 - }); + // grid-box + const gridBox = svgdoc.createElementNS(NS.SVG, 'rect'); + assignAttributes(gridBox, { + width: '100%', + height: '100%', + x: 0, + y: 0, + 'stroke-width': 0, + stroke: 'none', + fill: 'url(#gridpattern)', + style: 'pointer-events: none; display:visible;' + }); + $('#canvasGrid').append(gridBox); - var gridimg = svgdoc.createElementNS(NS.SVG, 'image'); - assignAttributes(gridimg, { - 'x': 0, - 'y': 0, - 'width': 100, - 'height': 100 - }); - gridPattern.appendChild(gridimg); - $('#svgroot defs').append(gridPattern); + function updateGrid (zoom) { + // TODO: Try this with elements, then compare performance difference + const unit = units[svgEditor.curConfig.baseUnit]; // 1 = 1px + const uMulti = unit * zoom; + // Calculate the main number interval + const rawM = 100 / uMulti; + let multi = 1; + for (let i = 0; i < intervals.length; i++) { + const num = intervals[i]; + multi = num; + if (rawM <= num) { + break; + } + } + const bigInt = multi * uMulti; - // grid-box - var gridBox = svgdoc.createElementNS(NS.SVG, 'rect'); - assignAttributes(gridBox, { - 'width': '100%', - 'height': '100%', - 'x': 0, - 'y': 0, - 'stroke-width': 0, - 'stroke': 'none', - 'fill': 'url(#gridpattern)', - 'style': 'pointer-events: none; display:visible;' - }); - $('#canvasGrid').append(gridBox); + // Set the canvas size to the width of the container + hcanvas.width = bigInt; + hcanvas.height = bigInt; + const ctx = hcanvas.getContext('2d'); + const curD = 0.5; + const part = bigInt / 10; - function updateGrid(zoom) { - var i; - // TODO: Try this with elements, then compare performance difference - var unit = units[svgEditor.curConfig.baseUnit]; // 1 = 1px - var u_multi = unit * zoom; - // Calculate the main number interval - var raw_m = 100 / u_multi; - var multi = 1; - for (i = 0; i < intervals.length; i++) { - var num = intervals[i]; - multi = num; - if (raw_m <= num) { - break; - } - } - var big_int = multi * u_multi; + ctx.globalAlpha = 0.2; + ctx.strokeStyle = svgEditor.curConfig.gridColor; + for (let i = 1; i < 10; i++) { + const subD = Math.round(part * i) + 0.5; + // const lineNum = (i % 2)?12:10; + const lineNum = 0; + ctx.moveTo(subD, bigInt); + ctx.lineTo(subD, lineNum); + ctx.moveTo(bigInt, subD); + ctx.lineTo(lineNum, subD); + } + ctx.stroke(); + ctx.beginPath(); + ctx.globalAlpha = 0.5; + ctx.moveTo(curD, bigInt); + ctx.lineTo(curD, 0); - // Set the canvas size to the width of the container - hcanvas.width = big_int; - hcanvas.height = big_int; - var ctx = hcanvas.getContext('2d'); - var cur_d = 0.5; - var part = big_int / 10; + ctx.moveTo(bigInt, curD); + ctx.lineTo(0, curD); + ctx.stroke(); - ctx.globalAlpha = 0.2; - ctx.strokeStyle = svgEditor.curConfig.gridColor; - for (i = 1; i < 10; i++) { - var sub_d = Math.round(part * i) + 0.5; - // var line_num = (i % 2)?12:10; - var line_num = 0; - ctx.moveTo(sub_d, big_int); - ctx.lineTo(sub_d, line_num); - ctx.moveTo(big_int, sub_d); - ctx.lineTo(line_num ,sub_d); - } - ctx.stroke(); - ctx.beginPath(); - ctx.globalAlpha = 0.5; - ctx.moveTo(cur_d, big_int); - ctx.lineTo(cur_d, 0); + const datauri = hcanvas.toDataURL('image/png'); + gridimg.setAttribute('width', bigInt); + gridimg.setAttribute('height', bigInt); + gridimg.parentNode.setAttribute('width', bigInt); + gridimg.parentNode.setAttribute('height', bigInt); + svgCanvas.setHref(gridimg, datauri); + } - ctx.moveTo(big_int, cur_d); - ctx.lineTo(0, cur_d); - ctx.stroke(); + function gridUpdate () { + if (showGrid) { + updateGrid(svgCanvas.getZoom()); + } + $('#canvasGrid').toggle(showGrid); + $('#view_grid').toggleClass('push_button_pressed tool_button'); + } + const buttons = [{ + id: 'view_grid', + icon: svgEditor.curConfig.extIconsPath + 'grid.png', + type: 'context', + panel: 'editor_panel', + events: { + click () { + svgEditor.curConfig.showGrid = showGrid = !showGrid; + gridUpdate(); + } + } + }]; + return { + name: strings.name, + svgicons: svgEditor.curConfig.extIconsPath + 'grid-icon.xml', - var datauri = hcanvas.toDataURL('image/png'); - gridimg.setAttribute('width', big_int); - gridimg.setAttribute('height', big_int); - gridimg.parentNode.setAttribute('width', big_int); - gridimg.parentNode.setAttribute('height', big_int); - svgCanvas.setHref(gridimg, datauri); - } - - function gridUpdate () { - if (showGrid) { - updateGrid(svgCanvas.getZoom()); - } - $('#canvasGrid').toggle(showGrid); - $('#view_grid').toggleClass('push_button_pressed tool_button'); - } - return { - name: 'view_grid', - svgicons: svgEditor.curConfig.extPath + 'grid-icon.xml', - - zoomChanged: function(zoom) { - if (showGrid) {updateGrid(zoom);} - }, - callback: function () { - if (showGrid) { - gridUpdate(); - } - }, - buttons: [{ - id: 'view_grid', - type: 'context', - panel: 'editor_panel', - title: 'Show/Hide Grid', - events: { - click: function() { - svgEditor.curConfig.showGrid = showGrid = !showGrid; - gridUpdate(); - } - } - }] - }; -}); + zoomChanged (zoom) { + if (showGrid) { updateGrid(zoom); } + }, + callback () { + if (showGrid) { + gridUpdate(); + } + }, + buttons: strings.buttons.map((button, i) => { + return Object.assign(buttons[i], button); + }) + }; + } +}; diff --git a/editor/extensions/ext-helloworld.js b/editor/extensions/ext-helloworld.js index eb67efce..172df74e 100644 --- a/editor/extensions/ext-helloworld.js +++ b/editor/extensions/ext-helloworld.js @@ -1,80 +1,92 @@ -/*globals svgEditor, svgCanvas, $*/ -/*jslint vars: true, eqeq: true*/ -/* +/* globals jQuery */ +/** * ext-helloworld.js * - * Licensed under the MIT License + * @license MIT * - * Copyright(c) 2010 Alexis Deveria + * @copyright 2010 Alexis Deveria * */ - -/* - This is a very basic SVG-Edit extension. It adds a "Hello World" button in - the left panel. Clicking on the button, and then the canvas will show the - user the point on the canvas that was clicked on. + +/** +* This is a very basic SVG-Edit extension. It adds a "Hello World" button in +* the left ("mode") panel. Clicking on the button, and then the canvas +* will show the user the point on the canvas that was clicked on. */ - -svgEditor.addExtension("Hello World", function() {'use strict'; +export default { + name: 'helloworld', + async init ({importLocale}) { + // See `/editor/extensions/ext-locale/helloworld/` + const strings = await importLocale(); + const svgEditor = this; + const $ = jQuery; + const svgCanvas = svgEditor.canvas; + return { + name: strings.name, + // For more notes on how to make an icon file, see the source of + // the helloworld-icon.xml + svgicons: svgEditor.curConfig.extIconsPath + 'helloworld-icon.xml', - return { - name: "Hello World", - // For more notes on how to make an icon file, see the source of - // the helloworld-icon.xml - svgicons: svgEditor.curConfig.extPath + "helloworld-icon.xml", - - // Multiple buttons can be added in this array - buttons: [{ - // Must match the icon ID in helloworld-icon.xml - id: "hello_world", - - // This indicates that the button will be added to the "mode" - // button panel on the left side - type: "mode", - - // Tooltip text - title: "Say 'Hello World'", - - // Events - events: { - 'click': function() { - // The action taken when the button is clicked on. - // For "mode" buttons, any other button will - // automatically be de-pressed. - svgCanvas.setMode("hello_world"); - } - } - }], - // This is triggered when the main mouse button is pressed down - // on the editor canvas (not the tool panels) - mouseDown: function() { - // Check the mode on mousedown - if(svgCanvas.getMode() == "hello_world") { - - // The returned object must include "started" with - // a value of true in order for mouseUp to be triggered - return {started: true}; - } - }, - - // This is triggered from anywhere, but "started" must have been set - // to true (see above). Note that "opts" is an object with event info - mouseUp: function(opts) { - // Check the mode on mouseup - if(svgCanvas.getMode() == "hello_world") { - var zoom = svgCanvas.getZoom(); - - // Get the actual coordinate by dividing by the zoom value - var x = opts.mouse_x / zoom; - var y = opts.mouse_y / zoom; - - var text = "Hello World!\n\nYou clicked here: " - + x + ", " + y; - - // Show the text using the custom alert function - $.alert(text); - } - } - }; -}); + // Multiple buttons can be added in this array + buttons: [{ + // Must match the icon ID in helloworld-icon.xml + id: 'hello_world', + // Fallback, e.g., for `file://` access + icon: svgEditor.curConfig.extIconsPath + 'helloworld.png', + + // This indicates that the button will be added to the "mode" + // button panel on the left side + type: 'mode', + + // Tooltip text + title: strings.buttons[0].title, + + // Events + events: { + click () { + // The action taken when the button is clicked on. + // For "mode" buttons, any other button will + // automatically be de-pressed. + svgCanvas.setMode('hello_world'); + } + } + }], + // This is triggered when the main mouse button is pressed down + // on the editor canvas (not the tool panels) + mouseDown () { + // Check the mode on mousedown + if (svgCanvas.getMode() === 'hello_world') { + // The returned object must include "started" with + // a value of true in order for mouseUp to be triggered + return {started: true}; + } + }, + + // This is triggered from anywhere, but "started" must have been set + // to true (see above). Note that "opts" is an object with event info + mouseUp (opts) { + // Check the mode on mouseup + if (svgCanvas.getMode() === 'hello_world') { + const zoom = svgCanvas.getZoom(); + + // Get the actual coordinate by dividing by the zoom value + const x = opts.mouse_x / zoom; + const y = opts.mouse_y / zoom; + + // We do our own formatting + let {text} = strings; + [ + ['x', x], + ['y', y] + ].forEach(([prop, val]) => { + text = text.replace('{' + prop + '}', val); + }); + + // Show the text using the custom alert function + $.alert(text); + } + } + }; + } +}; diff --git a/editor/extensions/ext-imagelib.js b/editor/extensions/ext-imagelib.js index f26e5c42..744a69a1 100644 --- a/editor/extensions/ext-imagelib.js +++ b/editor/extensions/ext-imagelib.js @@ -1,464 +1,458 @@ -/*globals $, svgEditor, svgedit, svgCanvas, DOMParser*/ -/*jslint vars: true, eqeq: true, es5: true, todo: true */ -/* - * ext-imagelib.js - * - * Licensed under the MIT License - * - * Copyright(c) 2010 Alexis Deveria - * - */ - -svgEditor.addExtension("imagelib", function() {'use strict'; - - var uiStrings = svgEditor.uiStrings; - - $.extend(uiStrings, { - imagelib: { - select_lib: 'Select an image library', - show_list: 'Show library list', - import_single: 'Import single', - import_multi: 'Import multiple', - open: 'Open as new document' - } - }); - - var img_libs = [{ - name: 'Demo library (local)', - url: svgEditor.curConfig.extPath + 'imagelib/index.html', - description: 'Demonstration library for SVG-edit on this server' - }, - { - name: 'IAN Symbol Libraries', - url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php', - description: 'Free library of illustrations' - }, - { - name: 'Openclipart', - url: 'https://openclipart.org/svgedit', - description: 'Share and Use Images. Over 50,000 Public Domain SVG Images and Growing.' - } - ]; - - function closeBrowser() { - $('#imgbrowse_holder').hide(); - } - - function importImage(url) { - var newImage = svgCanvas.addSvgElementFromJson({ - "element": "image", - "attr": { - "x": 0, - "y": 0, - "width": 0, - "height": 0, - "id": svgCanvas.getNextId(), - "style": "pointer-events:inherit" - } - }); - svgCanvas.clearSelection(); - svgCanvas.addToSelection([newImage]); - svgCanvas.setImageURL(url); - } - - var mode = 's'; - var multi_arr = []; - var tranfer_stopped = false; - var pending = {}; - var preview, submit; - - window.addEventListener("message", function(evt) { - // Receive postMessage data - var response = evt.data; - - if (!response || typeof response !== "string") { // Todo: Should namespace postMessage API for this extension and filter out here - // Do nothing - return; - } - try { // This block can be removed if embedAPI moves away from a string to an object (if IE9 support not needed) - var res = JSON.parse(response); - if (res.namespace) { // Part of embedAPI communications - return; - } - } - catch (e) {} - - var char1 = response.charAt(0); - var id; - var svg_str; - var img_str; - - if (char1 != "{" && tranfer_stopped) { - tranfer_stopped = false; - return; - } - - if (char1 == '|') { - var secondpos = response.indexOf('|', 1); - id = response.substr(1, secondpos-1); - response = response.substr(secondpos+1); - char1 = response.charAt(0); - } - - - // Hide possible transfer dialog box - $('#dialog_box').hide(); - var entry, cur_meta; - switch (char1) { - case '{': - // Metadata - tranfer_stopped = false; - cur_meta = JSON.parse(response); - - pending[cur_meta.id] = cur_meta; - - var name = (cur_meta.name || 'file'); - - var message = uiStrings.notification.retrieving.replace('%s', name); - - if (mode != 'm') { - $.process_cancel(message, function() { - tranfer_stopped = true; - // Should a message be sent back to the frame? - - $('#dialog_box').hide(); - }); - } else { - entry = $('
    ' + message + '
    ').data('id', cur_meta.id); - preview.append(entry); - cur_meta.entry = entry; - } - - return; - case '<': - svg_str = true; - break; - case 'd': - if (response.indexOf('data:image/svg+xml') === 0) { - var pre = 'data:image/svg+xml;base64,'; - var src = response.substring(pre.length); - response = svgedit.utilities.decode64(src); - svg_str = true; - break; - } else if (response.indexOf('data:image/') === 0) { - img_str = true; - break; - } - // Else fall through - default: - // TODO: See if there's a way to base64 encode the binary data stream -// var str = 'data:;base64,' + svgedit.utilities.encode64(response, true); - - // Assume it's raw image data -// importImage(str); - - // Don't give warning as postMessage may have been used by something else - if (mode !== 'm') { - closeBrowser(); - } else { - pending[id].entry.remove(); - } -// $.alert('Unexpected data was returned: ' + response, function() { -// if (mode !== 'm') { -// closeBrowser(); -// } else { -// pending[id].entry.remove(); -// } -// }); - return; - } - - switch (mode) { - case 's': - // Import one - if (svg_str) { - svgCanvas.importSvgString(response); - } else if (img_str) { - importImage(response); - } - closeBrowser(); - break; - case 'm': - // Import multiple - multi_arr.push([(svg_str ? 'svg' : 'img'), response]); - var title; - cur_meta = pending[id]; - if (svg_str) { - if (cur_meta && cur_meta.name) { - title = cur_meta.name; - } else { - // Try to find a title - var xml = new DOMParser().parseFromString(response, 'text/xml').documentElement; - title = $(xml).children('title').first().text() || '(SVG #' + response.length + ')'; - } - if (cur_meta) { - preview.children().each(function() { - if ($(this).data('id') == id) { - if (cur_meta.preview_url) { - $(this).html('' + title); - } else { - $(this).text(title); - } - submit.removeAttr('disabled'); - } - }); - } else { - preview.append('
    '+title+'
    '); - submit.removeAttr('disabled'); - } - } else { - if (cur_meta && cur_meta.preview_url) { - title = cur_meta.name || ''; - } - if (cur_meta && cur_meta.preview_url) { - entry = '' + title; - } else { - entry = ''; - } - - if (cur_meta) { - preview.children().each(function() { - if ($(this).data('id') == id) { - $(this).html(entry); - submit.removeAttr('disabled'); - } - }); - } else { - preview.append($('
    ').append(entry)); - submit.removeAttr('disabled'); - } - - } - break; - case 'o': - // Open - if (!svg_str) {break;} - svgEditor.openPrep(function(ok) { - if (!ok) {return;} - svgCanvas.clear(); - svgCanvas.setSvgString(response); - // updateCanvas(); - }); - closeBrowser(); - break; - } - }, true); - - function toggleMulti(show) { - - $('#lib_framewrap, #imglib_opts').css({right: (show ? 200 : 10)}); - if (!preview) { - preview = $('
    ').css({ - position: 'absolute', - top: 45, - right: 10, - width: 180, - bottom: 45, - background: '#fff', - overflow: 'auto' - }).insertAfter('#lib_framewrap'); - - submit = $('') - .appendTo('#imgbrowse') - .on("click touchend", function() { - $.each(multi_arr, function(i) { - var type = this[0]; - var data = this[1]; - if (type == 'svg') { - svgCanvas.importSvgString(data); - } else { - importImage(data); - } - svgCanvas.moveSelectedElements(i*20, i*20, false); - }); - preview.empty(); - multi_arr = []; - $('#imgbrowse_holder').hide(); - }).css({ - position: 'absolute', - bottom: 10, - right: -10 - }); - - } - - preview.toggle(show); - submit.toggle(show); - } - - function showBrowser() { - - var browser = $('#imgbrowse'); - if (!browser.length) { - $('
    \ -
    ').insertAfter('#svg_docprops'); - browser = $('#imgbrowse'); - - var all_libs = uiStrings.imagelib.select_lib; - - var lib_opts = $('
      ').appendTo(browser); - var frame = $(' - - - - - - - - - - - - - +

      This file frames all SVG-edit test pages. This should only include + tests known to work. These tests are known to pass 100% in the + following: + Firefox 3.6, Chrome 7, IE9 Preview 6 (1.9.8006.6000), Opera 10.63. + If a test is broken in this page, it is possible that YOU + broke it. Please do not submit code that breaks any of these tests.

      + + + + + + + + + + + + + + + + + - - \ No newline at end of file + diff --git a/test/all_tests.js b/test/all_tests.js new file mode 100644 index 00000000..2d45f3a4 --- /dev/null +++ b/test/all_tests.js @@ -0,0 +1,8 @@ +const iframes = document.querySelectorAll('iframe'); +[...iframes].forEach((f) => { + f.addEventListener('load', () => { + f.contentWindow.QUnit.done(() => { + f.style.height = (f.contentDocument.body.scrollHeight + 20) + 'px'; + }); + }); +}); diff --git a/test/browser-bugs/removeItem-bug.html b/test/browser-bugs/removeItem-bug.html new file mode 100644 index 00000000..3c844708 --- /dev/null +++ b/test/browser-bugs/removeItem-bug.html @@ -0,0 +1,21 @@ + + + + + removeItem and setAttribute test + + + + + Issue: + + Chromium 843901 + + + diff --git a/test/contextmenu_test.html b/test/contextmenu_test.html index 80cd87d0..93724ec4 100644 --- a/test/contextmenu_test.html +++ b/test/contextmenu_test.html @@ -3,81 +3,23 @@ Unit Tests for contextmenu.js - - + + + - - - + + -

      Unit Tests for contextmenu.js

      -

      -

      -
        - +

        Unit Tests for contextmenu.js

        +

        +

        +
          + diff --git a/test/contextmenu_test.js b/test/contextmenu_test.js new file mode 100644 index 00000000..c5391536 --- /dev/null +++ b/test/contextmenu_test.js @@ -0,0 +1,60 @@ +/* eslint-env qunit */ +import * as contextmenu from '../editor/contextmenu.js'; + +// log function +QUnit.log((details) => { + if (window.console && window.console.log) { + window.console.log(details.result + ' :: ' + details.message); + } +}); + +function tearDown () { + contextmenu.resetCustomMenus(); +} + +QUnit.module('svgedit.contextmenu'); + +QUnit.test('Test svgedit.contextmenu package', function (assert) { + assert.expect(4); + + assert.ok(contextmenu, 'contextmenu registered correctly'); + assert.ok(contextmenu.add, 'add registered correctly'); + assert.ok(contextmenu.hasCustomHandler, 'contextmenu hasCustomHandler registered correctly'); + assert.ok(contextmenu.getCustomHandler, 'contextmenu getCustomHandler registered correctly'); +}); + +QUnit.test('Test svgedit.contextmenu does not add invalid menu item', function (assert) { + assert.expect(3); + + contextmenu.add({id: 'justanid'}); + assert.ok(!contextmenu.hasCustomHandler('justanid'), 'menu item with just an id is invalid'); + + contextmenu.add({id: 'idandlabel', label: 'anicelabel'}); + assert.ok(!contextmenu.hasCustomHandler('idandlabel'), 'menu item with just an id and label is invalid'); + + contextmenu.add({id: 'idandlabel', label: 'anicelabel', action: 'notafunction'}); + assert.ok(!contextmenu.hasCustomHandler('idandlabel'), 'menu item with action that is not a function is invalid'); +}); + +QUnit.test('Test svgedit.contextmenu adds valid menu item', function (assert) { + assert.expect(2); + + const validItem = {id: 'valid', label: 'anicelabel', action () { alert('testing'); }}; + contextmenu.add(validItem); + + assert.ok(contextmenu.hasCustomHandler('valid'), 'Valid menu item is added.'); + assert.equal(contextmenu.getCustomHandler('valid'), validItem.action, 'Valid menu action is added.'); + tearDown(); +}); + +QUnit.test('Test svgedit.contextmenu rejects valid duplicate menu item id', function (assert) { + assert.expect(1); + + const validItem1 = {id: 'valid', label: 'anicelabel', action () { alert('testing'); }}; + const validItem2 = {id: 'valid', label: 'anicelabel', action () { alert('testingtwice'); }}; + contextmenu.add(validItem1); + contextmenu.add(validItem2); + + assert.equal(contextmenu.getCustomHandler('valid'), validItem1.action, 'duplicate menu item is rejected.'); + tearDown(); +}); diff --git a/test/coords_test.html b/test/coords_test.html index ea612439..d20f4273 100644 --- a/test/coords_test.html +++ b/test/coords_test.html @@ -3,359 +3,18 @@ Unit Tests for coords.js - - - - - - - - - - + + + - - + + - -

          Unit Tests for svgedit.coords

          -

          -

          -
            - + +

            Unit Tests for coords

            +

            +

            +
              + diff --git a/test/coords_test.js b/test/coords_test.js new file mode 100644 index 00000000..48837d3d --- /dev/null +++ b/test/coords_test.js @@ -0,0 +1,346 @@ +/* eslint-env qunit */ +import {NS} from '../editor/namespaces.js'; +import * as utilities from '../editor/utilities.js'; +import * as coords from '../editor/coords.js'; + +// log function +QUnit.log((details) => { + if (window.console && window.console.log) { + window.console.log(details.result + ' :: ' + details.message); + } +}); + +const root = document.getElementById('root'); +const svgroot = document.createElementNS(NS.SVG, 'svg'); +svgroot.id = 'svgroot'; +root.append(svgroot); +const svg = document.createElementNS(NS.SVG, 'svg'); +svgroot.append(svg); + +let elemId = 1; +function setUp () { + // Mock out editor context. + utilities.init( + /** + * @implements {module:utilities.EditorContext} + */ + { + getSVGRoot () { return svg; }, + getDOMDocument () { return null; }, + getDOMContainer () { return null; } + } + ); + coords.init( + /** + * @implements {module:coords.EditorContext} + */ + { + getGridSnapping () { return false; }, + getDrawing () { + return { + getNextId () { return '' + elemId++; } + }; + } + } + ); +} + +function tearDown () { + while (svg.hasChildNodes()) { + svg.firstChild.remove(); + } +} + +QUnit.test('Test remapElement(translate) for rect', function (assert) { + assert.expect(4); + + setUp(); + + const rect = document.createElementNS(NS.SVG, 'rect'); + rect.setAttribute('x', '200'); + rect.setAttribute('y', '150'); + rect.setAttribute('width', '250'); + rect.setAttribute('height', '120'); + svg.append(rect); + + const attrs = { + x: '200', + y: '150', + width: '125', + height: '75' + }; + + // Create a translate. + const m = svg.createSVGMatrix(); + m.a = 1; m.b = 0; + m.c = 0; m.d = 1; + m.e = 100; m.f = -50; + + coords.remapElement(rect, attrs, m); + + assert.equal(rect.getAttribute('x'), '300'); + assert.equal(rect.getAttribute('y'), '100'); + assert.equal(rect.getAttribute('width'), '125'); + assert.equal(rect.getAttribute('height'), '75'); + + tearDown(); +}); + +QUnit.test('Test remapElement(scale) for rect', function (assert) { + assert.expect(4); + setUp(); + + const rect = document.createElementNS(NS.SVG, 'rect'); + rect.setAttribute('width', '250'); + rect.setAttribute('height', '120'); + svg.append(rect); + + const attrs = { + x: '0', + y: '0', + width: '250', + height: '120' + }; + + // Create a translate. + const m = svg.createSVGMatrix(); + m.a = 2; m.b = 0; + m.c = 0; m.d = 0.5; + m.e = 0; m.f = 0; + + coords.remapElement(rect, attrs, m); + + assert.equal(rect.getAttribute('x'), '0'); + assert.equal(rect.getAttribute('y'), '0'); + assert.equal(rect.getAttribute('width'), '500'); + assert.equal(rect.getAttribute('height'), '60'); + + tearDown(); +}); + +QUnit.test('Test remapElement(translate) for circle', function (assert) { + assert.expect(3); + setUp(); + + const circle = document.createElementNS(NS.SVG, 'circle'); + circle.setAttribute('cx', '200'); + circle.setAttribute('cy', '150'); + circle.setAttribute('r', '125'); + svg.append(circle); + + const attrs = { + cx: '200', + cy: '150', + r: '125' + }; + + // Create a translate. + const m = svg.createSVGMatrix(); + m.a = 1; m.b = 0; + m.c = 0; m.d = 1; + m.e = 100; m.f = -50; + + coords.remapElement(circle, attrs, m); + + assert.equal(circle.getAttribute('cx'), '300'); + assert.equal(circle.getAttribute('cy'), '100'); + assert.equal(circle.getAttribute('r'), '125'); + + tearDown(); +}); + +QUnit.test('Test remapElement(scale) for circle', function (assert) { + assert.expect(3); + setUp(); + + const circle = document.createElementNS(NS.SVG, 'circle'); + circle.setAttribute('cx', '200'); + circle.setAttribute('cy', '150'); + circle.setAttribute('r', '250'); + svg.append(circle); + + const attrs = { + cx: '200', + cy: '150', + r: '250' + }; + + // Create a translate. + const m = svg.createSVGMatrix(); + m.a = 2; m.b = 0; + m.c = 0; m.d = 0.5; + m.e = 0; m.f = 0; + + coords.remapElement(circle, attrs, m); + + assert.equal(circle.getAttribute('cx'), '400'); + assert.equal(circle.getAttribute('cy'), '75'); + // Radius is the minimum that fits in the new bounding box. + assert.equal(circle.getAttribute('r'), '125'); + + tearDown(); +}); + +QUnit.test('Test remapElement(translate) for ellipse', function (assert) { + assert.expect(4); + setUp(); + + const ellipse = document.createElementNS(NS.SVG, 'ellipse'); + ellipse.setAttribute('cx', '200'); + ellipse.setAttribute('cy', '150'); + ellipse.setAttribute('rx', '125'); + ellipse.setAttribute('ry', '75'); + svg.append(ellipse); + + const attrs = { + cx: '200', + cy: '150', + rx: '125', + ry: '75' + }; + + // Create a translate. + const m = svg.createSVGMatrix(); + m.a = 1; m.b = 0; + m.c = 0; m.d = 1; + m.e = 100; m.f = -50; + + coords.remapElement(ellipse, attrs, m); + + assert.equal(ellipse.getAttribute('cx'), '300'); + assert.equal(ellipse.getAttribute('cy'), '100'); + assert.equal(ellipse.getAttribute('rx'), '125'); + assert.equal(ellipse.getAttribute('ry'), '75'); + + tearDown(); +}); + +QUnit.test('Test remapElement(scale) for ellipse', function (assert) { + assert.expect(4); + setUp(); + + const ellipse = document.createElementNS(NS.SVG, 'ellipse'); + ellipse.setAttribute('cx', '200'); + ellipse.setAttribute('cy', '150'); + ellipse.setAttribute('rx', '250'); + ellipse.setAttribute('ry', '120'); + svg.append(ellipse); + + const attrs = { + cx: '200', + cy: '150', + rx: '250', + ry: '120' + }; + + // Create a translate. + const m = svg.createSVGMatrix(); + m.a = 2; m.b = 0; + m.c = 0; m.d = 0.5; + m.e = 0; m.f = 0; + + coords.remapElement(ellipse, attrs, m); + + assert.equal(ellipse.getAttribute('cx'), '400'); + assert.equal(ellipse.getAttribute('cy'), '75'); + assert.equal(ellipse.getAttribute('rx'), '500'); + assert.equal(ellipse.getAttribute('ry'), '60'); + + tearDown(); +}); + +QUnit.test('Test remapElement(translate) for line', function (assert) { + assert.expect(4); + setUp(); + + const line = document.createElementNS(NS.SVG, 'line'); + line.setAttribute('x1', '50'); + line.setAttribute('y1', '100'); + line.setAttribute('x2', '120'); + line.setAttribute('y2', '200'); + svg.append(line); + + const attrs = { + x1: '50', + y1: '100', + x2: '120', + y2: '200' + }; + + // Create a translate. + const m = svg.createSVGMatrix(); + m.a = 1; m.b = 0; + m.c = 0; m.d = 1; + m.e = 100; m.f = -50; + + coords.remapElement(line, attrs, m); + + assert.equal(line.getAttribute('x1'), '150'); + assert.equal(line.getAttribute('y1'), '50'); + assert.equal(line.getAttribute('x2'), '220'); + assert.equal(line.getAttribute('y2'), '150'); + + tearDown(); +}); + +QUnit.test('Test remapElement(scale) for line', function (assert) { + assert.expect(4); + setUp(); + + const line = document.createElementNS(NS.SVG, 'line'); + line.setAttribute('x1', '50'); + line.setAttribute('y1', '100'); + line.setAttribute('x2', '120'); + line.setAttribute('y2', '200'); + svg.append(line); + + const attrs = { + x1: '50', + y1: '100', + x2: '120', + y2: '200' + }; + + // Create a translate. + const m = svg.createSVGMatrix(); + m.a = 2; m.b = 0; + m.c = 0; m.d = 0.5; + m.e = 0; m.f = 0; + + coords.remapElement(line, attrs, m); + + assert.equal(line.getAttribute('x1'), '100'); + assert.equal(line.getAttribute('y1'), '50'); + assert.equal(line.getAttribute('x2'), '240'); + assert.equal(line.getAttribute('y2'), '100'); + + tearDown(); +}); + +QUnit.test('Test remapElement(translate) for text', function (assert) { + assert.expect(2); + setUp(); + + const text = document.createElementNS(NS.SVG, 'text'); + text.setAttribute('x', '50'); + text.setAttribute('y', '100'); + svg.append(text); + + const attrs = { + x: '50', + y: '100' + }; + + // Create a translate. + const m = svg.createSVGMatrix(); + m.a = 1; m.b = 0; + m.c = 0; m.d = 1; + m.e = 100; m.f = -50; + + coords.remapElement(text, attrs, m); + + assert.equal(text.getAttribute('x'), '150'); + assert.equal(text.getAttribute('y'), '50'); + + tearDown(); +}); diff --git a/test/draw_test.html b/test/draw_test.html index 87a5f3be..2c7d0234 100644 --- a/test/draw_test.html +++ b/test/draw_test.html @@ -3,820 +3,17 @@ Unit Tests for draw.js - - - - - - - - - - - - - + + + + + -

              Unit Tests for draw.js

              -

              -

              -
                - +

                Unit Tests for draw.js

                +

                +

                +
                  + diff --git a/test/draw_test.js b/test/draw_test.js new file mode 100644 index 00000000..479041df --- /dev/null +++ b/test/draw_test.js @@ -0,0 +1,832 @@ +/* eslint-env qunit */ +import {NS} from '../editor/namespaces.js'; +import * as draw from '../editor/draw.js'; +import * as units from '../editor/units.js'; + +import sinon from '../node_modules/sinon/pkg/sinon-esm.js'; +import sinonTest from '../node_modules/sinon-test/dist/sinon-test-es.js'; +import sinonQunit from './sinon/sinon-qunit.js'; + +sinon.test = sinonTest(sinon, { + injectIntoThis: true, + injectInto: null, + properties: ['spy', 'stub', 'mock', 'clock', 'sandbox'], + useFakeTimers: false, + useFakeServer: false +}); +sinonQunit({sinon, QUnit}); + +// log function +QUnit.log((details) => { + if (window.console && window.console.log) { + window.console.log(details.result + ' :: ' + details.message); + } +}); + +const LAYER_CLASS = draw.Layer.CLASS_NAME; +const NONCE = 'foo'; +const LAYER1 = 'Layer 1'; +const LAYER2 = 'Layer 2'; +const LAYER3 = 'Layer 3'; +const PATH_ATTR = { + // clone will convert relative to absolute, so the test for equality fails. + // d: 'm7.38867,57.38867c0,-27.62431 22.37569,-50 50,-50c27.62431,0 50,22.37569 50,50c0,27.62431 -22.37569,50 -50,50c-27.62431,0 -50,-22.37569 -50,-50z', + d: 'M7.389,57.389C7.389,29.764 29.764,7.389 57.389,7.389C85.013,7.389 107.389,29.764 107.389,57.389C107.389,85.013 85.013,107.389 57.389,107.389C29.764,107.389 7.389,85.013 7.389,57.389z', + transform: 'rotate(45 57.388671875000036,57.388671874999986) ', + 'stroke-width': '5', + stroke: '#660000', + fill: '#ff0000' +}; + +const svg = document.createElementNS(NS.SVG, 'svg'); +const sandbox = document.getElementById('sandbox'); +// Firefox throws exception in getBBox() when svg is not attached to DOM. +sandbox.append(svg); + +// Set up with nonce. +const svgN = document.createElementNS(NS.SVG, 'svg'); +svgN.setAttributeNS(NS.XMLNS, 'xmlns:se', NS.SE); +svgN.setAttributeNS(NS.SE, 'se:nonce', NONCE); + +units.init( + /** + * @implements {module:units.ElementContainer} + */ + { + // used by units.shortFloat - call path: cloneLayer -> copyElem -> convertPath -> pathDSegment -> shortFloat + getRoundDigits () { return 3; } + } +); + +// Simplifying from svgcanvas.js usage +const idprefix = 'svg_'; +const svgcontent = document.createElementNS(NS.SVG, 'svg'); +const currentDrawing_ = new draw.Drawing(svgcontent, idprefix); +const getCurrentDrawing = function () { + return currentDrawing_; +}; +const setCurrentGroup = (cg) => { +}; +draw.init( + /** + * @implements {module:draw.DrawCanvasInit} + */ + { + getCurrentDrawing, + setCurrentGroup + } +); + +function createSVGElement (jsonMap) { + const elem = document.createElementNS(NS.SVG, jsonMap['element']); + for (const attr in jsonMap['attr']) { + elem.setAttribute(attr, jsonMap['attr'][attr]); + } + return elem; +} + +const setupSVGWith3Layers = function (svgElem) { + const layer1 = document.createElementNS(NS.SVG, 'g'); + const layer1Title = document.createElementNS(NS.SVG, 'title'); + layer1Title.append(document.createTextNode(LAYER1)); + layer1.append(layer1Title); + svgElem.append(layer1); + + const layer2 = document.createElementNS(NS.SVG, 'g'); + const layer2Title = document.createElementNS(NS.SVG, 'title'); + layer2Title.append(document.createTextNode(LAYER2)); + layer2.append(layer2Title); + svgElem.append(layer2); + + const layer3 = document.createElementNS(NS.SVG, 'g'); + const layer3Title = document.createElementNS(NS.SVG, 'title'); + layer3Title.append(document.createTextNode(LAYER3)); + layer3.append(layer3Title); + svgElem.append(layer3); + + return [layer1, layer2, layer3]; +}; + +const createSomeElementsInGroup = function (group) { + group.append( + createSVGElement({ + element: 'path', + attr: PATH_ATTR + }), + // createSVGElement({ + // element: 'path', + // attr: {d: 'M0,1L2,3'} + // }), + createSVGElement({ + element: 'rect', + attr: {x: '0', y: '1', width: '5', height: '10'} + }), + createSVGElement({ + element: 'line', + attr: {x1: '0', y1: '1', x2: '5', y2: '6'} + }) + ); + + const g = createSVGElement({ + element: 'g', + attr: {} + }); + g.append(createSVGElement({ + element: 'rect', + attr: {x: '0', y: '1', width: '5', height: '10'} + })); + group.append(g); + return 4; +}; + +const cleanupSVG = function (svgElem) { + while (svgElem.firstChild) { svgElem.firstChild.remove(); } +}; + +QUnit.module('svgedit.draw.Drawing', { + beforeEach () { + }, + afterEach () { + } +}); + +QUnit.test('Test draw module', function (assert) { + assert.expect(4); + + assert.ok(draw); + assert.equal(typeof draw, typeof {}); + + assert.ok(draw.Drawing); + assert.equal(typeof draw.Drawing, typeof function () {}); +}); + +QUnit.test('Test document creation', function (assert) { + assert.expect(3); + + let doc; + try { + doc = new draw.Drawing(); + assert.ok(false, 'Created drawing without a valid element'); + } catch (e) { + assert.ok(true); + } + + try { + doc = new draw.Drawing(svg); + assert.ok(doc); + assert.equal(typeof doc, typeof {}); + } catch (e) { + assert.ok(false, 'Could not create document from valid element: ' + e); + } +}); + +QUnit.test('Test nonce', function (assert) { + assert.expect(7); + + let doc = new draw.Drawing(svg); + assert.equal(doc.getNonce(), ''); + + doc = new draw.Drawing(svgN); + assert.equal(doc.getNonce(), NONCE); + assert.equal(doc.getSvgElem().getAttributeNS(NS.SE, 'nonce'), NONCE); + + doc.clearNonce(); + assert.ok(!doc.getNonce()); + assert.ok(!doc.getSvgElem().getAttributeNS(NS.SE, 'se:nonce')); + + doc.setNonce(NONCE); + assert.equal(doc.getNonce(), NONCE); + assert.equal(doc.getSvgElem().getAttributeNS(NS.SE, 'nonce'), NONCE); +}); + +QUnit.test('Test getId() and getNextId() without nonce', function (assert) { + assert.expect(7); + + const elem2 = document.createElementNS(NS.SVG, 'circle'); + elem2.id = 'svg_2'; + svg.append(elem2); + + const doc = new draw.Drawing(svg); + + assert.equal(doc.getId(), 'svg_0'); + + assert.equal(doc.getNextId(), 'svg_1'); + assert.equal(doc.getId(), 'svg_1'); + + assert.equal(doc.getNextId(), 'svg_3'); + assert.equal(doc.getId(), 'svg_3'); + + assert.equal(doc.getNextId(), 'svg_4'); + assert.equal(doc.getId(), 'svg_4'); + // clean out svg document + cleanupSVG(svg); +}); + +QUnit.test('Test getId() and getNextId() with prefix without nonce', function (assert) { + assert.expect(7); + + const prefix = 'Bar-'; + const doc = new draw.Drawing(svg, prefix); + + assert.equal(doc.getId(), prefix + '0'); + + assert.equal(doc.getNextId(), prefix + '1'); + assert.equal(doc.getId(), prefix + '1'); + + assert.equal(doc.getNextId(), prefix + '2'); + assert.equal(doc.getId(), prefix + '2'); + + assert.equal(doc.getNextId(), prefix + '3'); + assert.equal(doc.getId(), prefix + '3'); + + cleanupSVG(svg); +}); + +QUnit.test('Test getId() and getNextId() with nonce', function (assert) { + assert.expect(7); + + const prefix = 'svg_' + NONCE; + + const elem2 = document.createElementNS(NS.SVG, 'circle'); + elem2.id = prefix + '_2'; + svgN.append(elem2); + + const doc = new draw.Drawing(svgN); + + assert.equal(doc.getId(), prefix + '_0'); + + assert.equal(doc.getNextId(), prefix + '_1'); + assert.equal(doc.getId(), prefix + '_1'); + + assert.equal(doc.getNextId(), prefix + '_3'); + assert.equal(doc.getId(), prefix + '_3'); + + assert.equal(doc.getNextId(), prefix + '_4'); + assert.equal(doc.getId(), prefix + '_4'); + + cleanupSVG(svgN); +}); + +QUnit.test('Test getId() and getNextId() with prefix with nonce', function (assert) { + assert.expect(7); + + const PREFIX = 'Bar-'; + const doc = new draw.Drawing(svgN, PREFIX); + + const prefix = PREFIX + NONCE + '_'; + assert.equal(doc.getId(), prefix + '0'); + + assert.equal(doc.getNextId(), prefix + '1'); + assert.equal(doc.getId(), prefix + '1'); + + assert.equal(doc.getNextId(), prefix + '2'); + assert.equal(doc.getId(), prefix + '2'); + + assert.equal(doc.getNextId(), prefix + '3'); + assert.equal(doc.getId(), prefix + '3'); + + cleanupSVG(svgN); +}); + +QUnit.test('Test releaseId()', function (assert) { + assert.expect(6); + + const doc = new draw.Drawing(svg); + + const firstId = doc.getNextId(); + /* const secondId = */ doc.getNextId(); + + const result = doc.releaseId(firstId); + assert.ok(result); + assert.equal(doc.getNextId(), firstId); + assert.equal(doc.getNextId(), 'svg_3'); + + assert.ok(!doc.releaseId('bad-id')); + assert.ok(doc.releaseId(firstId)); + assert.ok(!doc.releaseId(firstId)); + + cleanupSVG(svg); +}); + +QUnit.test('Test getNumLayers', function (assert) { + assert.expect(3); + const drawing = new draw.Drawing(svg); + assert.equal(typeof drawing.getNumLayers, typeof function () {}); + assert.equal(drawing.getNumLayers(), 0); + + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.equal(drawing.getNumLayers(), 3); + + cleanupSVG(svg); +}); + +QUnit.test('Test hasLayer', function (assert) { + assert.expect(5); + + setupSVGWith3Layers(svg); + const drawing = new draw.Drawing(svg); + drawing.identifyLayers(); + + assert.equal(typeof drawing.hasLayer, typeof function () {}); + assert.ok(!drawing.hasLayer('invalid-layer')); + + assert.ok(drawing.hasLayer(LAYER3)); + assert.ok(drawing.hasLayer(LAYER2)); + assert.ok(drawing.hasLayer(LAYER1)); + + cleanupSVG(svg); +}); + +QUnit.test('Test identifyLayers() with empty document', function (assert) { + assert.expect(11); + + const drawing = new draw.Drawing(svg); + assert.equal(drawing.getCurrentLayer(), null); + // By default, an empty document gets an empty group created. + drawing.identifyLayers(); + + // Check that element now has one child node + assert.ok(drawing.getSvgElem().hasChildNodes()); + assert.equal(drawing.getSvgElem().childNodes.length, 1); + + // Check that all_layers are correctly set up. + assert.equal(drawing.getNumLayers(), 1); + const emptyLayer = drawing.all_layers[0]; + assert.ok(emptyLayer); + const layerGroup = emptyLayer.getGroup(); + assert.equal(layerGroup, drawing.getSvgElem().firstChild); + assert.equal(layerGroup.tagName, 'g'); + assert.equal(layerGroup.getAttribute('class'), LAYER_CLASS); + assert.ok(layerGroup.hasChildNodes()); + assert.equal(layerGroup.childNodes.length, 1); + const firstChild = layerGroup.childNodes.item(0); + assert.equal(firstChild.tagName, 'title'); + + cleanupSVG(svg); +}); + +QUnit.test('Test identifyLayers() with some layers', function (assert) { + assert.expect(8); + + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + + assert.equal(svg.childNodes.length, 3); + + drawing.identifyLayers(); + + assert.equal(drawing.getNumLayers(), 3); + assert.equal(drawing.all_layers[0].getGroup(), svg.childNodes.item(0)); + assert.equal(drawing.all_layers[1].getGroup(), svg.childNodes.item(1)); + assert.equal(drawing.all_layers[2].getGroup(), svg.childNodes.item(2)); + + assert.equal(drawing.all_layers[0].getGroup().getAttribute('class'), LAYER_CLASS); + assert.equal(drawing.all_layers[1].getGroup().getAttribute('class'), LAYER_CLASS); + assert.equal(drawing.all_layers[2].getGroup().getAttribute('class'), LAYER_CLASS); + + cleanupSVG(svg); +}); + +QUnit.test('Test identifyLayers() with some layers and orphans', function (assert) { + assert.expect(14); + + setupSVGWith3Layers(svg); + + const orphan1 = document.createElementNS(NS.SVG, 'rect'); + const orphan2 = document.createElementNS(NS.SVG, 'rect'); + svg.append(orphan1, orphan2); + + assert.equal(svg.childNodes.length, 5); + + const drawing = new draw.Drawing(svg); + drawing.identifyLayers(); + + assert.equal(drawing.getNumLayers(), 4); + assert.equal(drawing.all_layers[0].getGroup(), svg.childNodes.item(0)); + assert.equal(drawing.all_layers[1].getGroup(), svg.childNodes.item(1)); + assert.equal(drawing.all_layers[2].getGroup(), svg.childNodes.item(2)); + assert.equal(drawing.all_layers[3].getGroup(), svg.childNodes.item(3)); + + assert.equal(drawing.all_layers[0].getGroup().getAttribute('class'), LAYER_CLASS); + assert.equal(drawing.all_layers[1].getGroup().getAttribute('class'), LAYER_CLASS); + assert.equal(drawing.all_layers[2].getGroup().getAttribute('class'), LAYER_CLASS); + assert.equal(drawing.all_layers[3].getGroup().getAttribute('class'), LAYER_CLASS); + + const layer4 = drawing.all_layers[3].getGroup(); + assert.equal(layer4.tagName, 'g'); + assert.equal(layer4.childNodes.length, 3); + assert.equal(layer4.childNodes.item(1), orphan1); + assert.equal(layer4.childNodes.item(2), orphan2); + + cleanupSVG(svg); +}); + +QUnit.test('Test getLayerName()', function (assert) { + assert.expect(4); + + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + + drawing.identifyLayers(); + + assert.equal(drawing.getNumLayers(), 3); + assert.equal(drawing.getLayerName(0), LAYER1); + assert.equal(drawing.getLayerName(1), LAYER2); + assert.equal(drawing.getLayerName(2), LAYER3); + + cleanupSVG(svg); +}); + +QUnit.test('Test getCurrentLayer()', function (assert) { + assert.expect(4); + + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.getCurrentLayer); + assert.equal(typeof drawing.getCurrentLayer, typeof function () {}); + assert.ok(drawing.getCurrentLayer()); + assert.equal(drawing.getCurrentLayer(), drawing.all_layers[2].getGroup()); + + cleanupSVG(svg); +}); + +QUnit.test('Test setCurrentLayer() and getCurrentLayerName()', function (assert) { + assert.expect(6); + + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.setCurrentLayer); + assert.equal(typeof drawing.setCurrentLayer, typeof function () {}); + + drawing.setCurrentLayer(LAYER2); + assert.equal(drawing.getCurrentLayerName(), LAYER2); + assert.equal(drawing.getCurrentLayer(), drawing.all_layers[1].getGroup()); + + drawing.setCurrentLayer(LAYER3); + assert.equal(drawing.getCurrentLayerName(), LAYER3); + assert.equal(drawing.getCurrentLayer(), drawing.all_layers[2].getGroup()); + + cleanupSVG(svg); +}); + +QUnit.test('Test setCurrentLayerName()', function (assert) { + const mockHrService = { + changeElement: this.spy() + }; + + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.setCurrentLayerName); + assert.equal(typeof drawing.setCurrentLayerName, typeof function () {}); + + const oldName = drawing.getCurrentLayerName(); + const newName = 'New Name'; + assert.ok(drawing.layer_map[oldName]); + assert.equal(drawing.layer_map[newName], undefined); // newName shouldn't exist. + const result = drawing.setCurrentLayerName(newName, mockHrService); + assert.equal(result, newName); + assert.equal(drawing.getCurrentLayerName(), newName); + // Was the map updated? + assert.equal(drawing.layer_map[oldName], undefined); + assert.equal(drawing.layer_map[newName], drawing.current_layer); + // Was mockHrService called? + assert.ok(mockHrService.changeElement.calledOnce); + assert.equal(oldName, mockHrService.changeElement.getCall(0).args[1]['#text']); + assert.equal(newName, mockHrService.changeElement.getCall(0).args[0].textContent); + + cleanupSVG(svg); +}); + +QUnit.test('Test createLayer()', function (assert) { + assert.expect(10); + + const mockHrService = { + startBatchCommand: this.spy(), + endBatchCommand: this.spy(), + insertElement: this.spy() + }; + + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.createLayer); + assert.equal(typeof drawing.createLayer, typeof function () {}); + + const NEW_LAYER_NAME = 'Layer A'; + const layerG = drawing.createLayer(NEW_LAYER_NAME, mockHrService); + assert.equal(drawing.getNumLayers(), 4); + assert.equal(layerG, drawing.getCurrentLayer()); + assert.equal(layerG.getAttribute('class'), LAYER_CLASS); + assert.equal(NEW_LAYER_NAME, drawing.getCurrentLayerName()); + assert.equal(NEW_LAYER_NAME, drawing.getLayerName(3)); + + assert.equal(layerG, mockHrService.insertElement.getCall(0).args[0]); + assert.ok(mockHrService.startBatchCommand.calledOnce); + assert.ok(mockHrService.endBatchCommand.calledOnce); + + cleanupSVG(svg); +}); + +QUnit.test('Test mergeLayer()', function (assert) { + const mockHrService = { + startBatchCommand: this.spy(), + endBatchCommand: this.spy(), + moveElement: this.spy(), + removeElement: this.spy() + }; + + const drawing = new draw.Drawing(svg); + const layers = setupSVGWith3Layers(svg); + const elementCount = createSomeElementsInGroup(layers[2]) + 1; // +1 for title element + assert.equal(layers[1].childElementCount, 1); + assert.equal(layers[2].childElementCount, elementCount); + drawing.identifyLayers(); + assert.equal(drawing.getCurrentLayer(), layers[2]); + + assert.ok(drawing.mergeLayer); + assert.equal(typeof drawing.mergeLayer, typeof function () {}); + + drawing.mergeLayer(mockHrService); + + assert.equal(drawing.getNumLayers(), 2); + assert.equal(svg.childElementCount, 2); + assert.equal(drawing.getCurrentLayer(), layers[1]); + assert.equal(layers[1].childElementCount, elementCount); + + // check history record + assert.ok(mockHrService.startBatchCommand.calledOnce); + assert.ok(mockHrService.endBatchCommand.calledOnce); + assert.equal(mockHrService.startBatchCommand.getCall(0).args[0], 'Merge Layer'); + assert.equal(mockHrService.moveElement.callCount, elementCount - 1); // -1 because the title was not moved. + assert.equal(mockHrService.removeElement.callCount, 2); // remove group and title. + + cleanupSVG(svg); +}); + +QUnit.test('Test mergeLayer() when no previous layer to merge', function (assert) { + const mockHrService = { + startBatchCommand: this.spy(), + endBatchCommand: this.spy(), + moveElement: this.spy(), + removeElement: this.spy() + }; + + const drawing = new draw.Drawing(svg); + const layers = setupSVGWith3Layers(svg); + drawing.identifyLayers(); + drawing.setCurrentLayer(LAYER1); + assert.equal(drawing.getCurrentLayer(), layers[0]); + + drawing.mergeLayer(mockHrService); + + assert.equal(drawing.getNumLayers(), 3); + assert.equal(svg.childElementCount, 3); + assert.equal(drawing.getCurrentLayer(), layers[0]); + assert.equal(layers[0].childElementCount, 1); + assert.equal(layers[1].childElementCount, 1); + assert.equal(layers[2].childElementCount, 1); + + // check history record + assert.equal(mockHrService.startBatchCommand.callCount, 0); + assert.equal(mockHrService.endBatchCommand.callCount, 0); + assert.equal(mockHrService.moveElement.callCount, 0); + assert.equal(mockHrService.removeElement.callCount, 0); + + cleanupSVG(svg); +}); + +QUnit.test('Test mergeAllLayers()', function (assert) { + const mockHrService = { + startBatchCommand: this.spy(), + endBatchCommand: this.spy(), + moveElement: this.spy(), + removeElement: this.spy() + }; + + const drawing = new draw.Drawing(svg); + const layers = setupSVGWith3Layers(svg); + const elementCount = createSomeElementsInGroup(layers[0]) + 1; // +1 for title element + createSomeElementsInGroup(layers[1]); + createSomeElementsInGroup(layers[2]); + assert.equal(layers[0].childElementCount, elementCount); + assert.equal(layers[1].childElementCount, elementCount); + assert.equal(layers[2].childElementCount, elementCount); + drawing.identifyLayers(); + + assert.ok(drawing.mergeAllLayers); + assert.equal(typeof drawing.mergeAllLayers, typeof function () {}); + + drawing.mergeAllLayers(mockHrService); + + assert.equal(drawing.getNumLayers(), 1); + assert.equal(svg.childElementCount, 1); + assert.equal(drawing.getCurrentLayer(), layers[0]); + assert.equal(layers[0].childElementCount, elementCount * 3 - 2); // -2 because two titles were deleted. + + // check history record + assert.equal(mockHrService.startBatchCommand.callCount, 3); // mergeAllLayers + 2 * mergeLayer + assert.equal(mockHrService.endBatchCommand.callCount, 3); + assert.equal(mockHrService.startBatchCommand.getCall(0).args[0], 'Merge all Layers'); + assert.equal(mockHrService.startBatchCommand.getCall(1).args[0], 'Merge Layer'); + assert.equal(mockHrService.startBatchCommand.getCall(2).args[0], 'Merge Layer'); + // moveElement count is times 3 instead of 2, because one layer's elements were moved twice. + // moveElement count is minus 3 because the three titles were not moved. + assert.equal(mockHrService.moveElement.callCount, elementCount * 3 - 3); + assert.equal(mockHrService.removeElement.callCount, 2 * 2); // remove group and title twice. + + cleanupSVG(svg); +}); + +QUnit.test('Test cloneLayer()', function (assert) { + const mockHrService = { + startBatchCommand: this.spy(), + endBatchCommand: this.spy(), + insertElement: this.spy() + }; + + const drawing = new draw.Drawing(svg); + const layers = setupSVGWith3Layers(svg); + const layer3 = layers[2]; + const elementCount = createSomeElementsInGroup(layer3) + 1; // +1 for title element + assert.equal(layer3.childElementCount, elementCount); + drawing.identifyLayers(); + + assert.ok(drawing.cloneLayer); + assert.equal(typeof drawing.cloneLayer, typeof function () {}); + + const clone = drawing.cloneLayer('clone', mockHrService); + + assert.equal(drawing.getNumLayers(), 4); + assert.equal(svg.childElementCount, 4); + assert.equal(drawing.getCurrentLayer(), clone); + assert.equal(clone.childElementCount, elementCount); + + // check history record + assert.ok(mockHrService.startBatchCommand.calledOnce); // mergeAllLayers + 2 * mergeLayer + assert.ok(mockHrService.endBatchCommand.calledOnce); + assert.equal(mockHrService.startBatchCommand.getCall(0).args[0], 'Duplicate Layer'); + assert.equal(mockHrService.insertElement.callCount, 1); + assert.equal(mockHrService.insertElement.getCall(0).args[0], clone); + + // check that path is cloned properly + assert.equal(clone.childNodes.length, elementCount); + const path = clone.childNodes[1]; + assert.equal(path.id, 'svg_1'); + assert.equal(path.getAttribute('d'), PATH_ATTR.d); + assert.equal(path.getAttribute('transform'), PATH_ATTR.transform); + assert.equal(path.getAttribute('fill'), PATH_ATTR.fill); + assert.equal(path.getAttribute('stroke'), PATH_ATTR.stroke); + assert.equal(path.getAttribute('stroke-width'), PATH_ATTR['stroke-width']); + + // check that g is cloned properly + const g = clone.childNodes[4]; + assert.equal(g.childNodes.length, 1); + assert.equal(g.id, 'svg_4'); + + cleanupSVG(svg); +}); + +QUnit.test('Test getLayerVisibility()', function (assert) { + assert.expect(5); + + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.getLayerVisibility); + assert.equal(typeof drawing.getLayerVisibility, typeof function () {}); + assert.ok(drawing.getLayerVisibility(LAYER1)); + assert.ok(drawing.getLayerVisibility(LAYER2)); + assert.ok(drawing.getLayerVisibility(LAYER3)); + + cleanupSVG(svg); +}); + +QUnit.test('Test setLayerVisibility()', function (assert) { + assert.expect(6); + + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.setLayerVisibility); + assert.equal(typeof drawing.setLayerVisibility, typeof function () {}); + + drawing.setLayerVisibility(LAYER3, false); + drawing.setLayerVisibility(LAYER2, true); + drawing.setLayerVisibility(LAYER1, false); + + assert.ok(!drawing.getLayerVisibility(LAYER1)); + assert.ok(drawing.getLayerVisibility(LAYER2)); + assert.ok(!drawing.getLayerVisibility(LAYER3)); + + drawing.setLayerVisibility(LAYER3, 'test-string'); + assert.ok(!drawing.getLayerVisibility(LAYER3)); + + cleanupSVG(svg); +}); + +QUnit.test('Test getLayerOpacity()', function (assert) { + assert.expect(5); + + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.getLayerOpacity); + assert.equal(typeof drawing.getLayerOpacity, typeof function () {}); + assert.strictEqual(drawing.getLayerOpacity(LAYER1), 1.0); + assert.strictEqual(drawing.getLayerOpacity(LAYER2), 1.0); + assert.strictEqual(drawing.getLayerOpacity(LAYER3), 1.0); + + cleanupSVG(svg); +}); + +QUnit.test('Test setLayerOpacity()', function (assert) { + assert.expect(6); + + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + assert.ok(drawing.setLayerOpacity); + assert.equal(typeof drawing.setLayerOpacity, typeof function () {}); + + drawing.setLayerOpacity(LAYER1, 0.4); + drawing.setLayerOpacity(LAYER2, 'invalid-string'); + drawing.setLayerOpacity(LAYER3, -1.4); + + assert.strictEqual(drawing.getLayerOpacity(LAYER1), 0.4); + console.log('layer2 opacity ' + drawing.getLayerOpacity(LAYER2)); + assert.strictEqual(drawing.getLayerOpacity(LAYER2), 1.0); + assert.strictEqual(drawing.getLayerOpacity(LAYER3), 1.0); + + drawing.setLayerOpacity(LAYER3, 100); + assert.strictEqual(drawing.getLayerOpacity(LAYER3), 1.0); + + cleanupSVG(svg); +}); + +QUnit.test('Test deleteCurrentLayer()', function (assert) { + assert.expect(6); + + const drawing = new draw.Drawing(svg); + setupSVGWith3Layers(svg); + drawing.identifyLayers(); + + drawing.setCurrentLayer(LAYER2); + + const curLayer = drawing.getCurrentLayer(); + assert.equal(curLayer, drawing.all_layers[1].getGroup()); + const deletedLayer = drawing.deleteCurrentLayer(); + + assert.equal(curLayer, deletedLayer); + assert.equal(drawing.getNumLayers(), 2); + assert.equal(LAYER1, drawing.all_layers[0].getName()); + assert.equal(LAYER3, drawing.all_layers[1].getName()); + assert.equal(drawing.getCurrentLayer(), drawing.all_layers[1].getGroup()); +}); + +QUnit.test('Test svgedit.draw.randomizeIds()', function (assert) { + assert.expect(9); + + // Confirm in LET_DOCUMENT_DECIDE mode that the document decides + // if there is a nonce. + let drawing = new draw.Drawing(svgN.cloneNode(true)); + assert.ok(!!drawing.getNonce()); + + drawing = new draw.Drawing(svg.cloneNode(true)); + assert.ok(!drawing.getNonce()); + + // Confirm that a nonce is set once we're in ALWAYS_RANDOMIZE mode. + draw.randomizeIds(true, drawing); + assert.ok(!!drawing.getNonce()); + + // Confirm new drawings in ALWAYS_RANDOMIZE mode have a nonce. + drawing = new draw.Drawing(svg.cloneNode(true)); + assert.ok(!!drawing.getNonce()); + + drawing.clearNonce(); + assert.ok(!drawing.getNonce()); + + // Confirm new drawings in NEVER_RANDOMIZE mode do not have a nonce + // but that their se:nonce attribute is left alone. + draw.randomizeIds(false, drawing); + assert.ok(!drawing.getNonce()); + assert.ok(!!drawing.getSvgElem().getAttributeNS(NS.SE, 'nonce')); + + drawing = new draw.Drawing(svg.cloneNode(true)); + assert.ok(!drawing.getNonce()); + + drawing = new draw.Drawing(svgN.cloneNode(true)); + assert.ok(!drawing.getNonce()); +}); diff --git a/test/history_test.html b/test/history_test.html index a876b106..faefe45a 100644 --- a/test/history_test.html +++ b/test/history_test.html @@ -3,589 +3,24 @@ Unit Tests for history.js - - - - - - + + + + + -

                  Unit Tests for history.js

                  -

                  -

                  -
                    -