diff --git a/plugins/svg-render/CHANGELOG.md b/plugins/svg-render/CHANGELOG.md
new file mode 100644
index 0000000..158afef
--- /dev/null
+++ b/plugins/svg-render/CHANGELOG.md
@@ -0,0 +1,19 @@
+# Changelog
+
+## [Unreleased]
+
+## [0.2.0]
+
+- Set vizzu-0.10.x as a peer dependency
+
+## [0.1.1]
+
+### Fixed
+
+- Fixed missing `d.ts` and `js.map` files
+
+## [0.1.0]
+
+### Added
+
+- First release, set vizzu-0.9.x as a peer dependency
diff --git a/plugins/svg-render/README.md b/plugins/svg-render/README.md
new file mode 100644
index 0000000..266aa07
--- /dev/null
+++ b/plugins/svg-render/README.md
@@ -0,0 +1,67 @@
+# Vizzu video capture plugin
+
+This plugin allows you to create videos from chart animations in Vizzu.
+
+## Install
+
+```sh
+$ npm install @vizzu/video-capture
+```
+
+## Usage
+
+To use the plugin, simply add it to your Vizzu instance as a feature:
+
+```javascript
+import { VideoCapture } from '@vizzu/video-capture'
+
+await chart.initializing
+chart.features(new VideoCapture(), true)
+```
+
+Once registered, you can start and stop the recording as needed:
+
+```javascript
+const anim = chart.animate({
+ data,
+ config: {
+ x: 'Year',
+ y: ['Value 2 (+)', 'Joy factors'],
+ color: 'Joy factors',
+ title: 'Video Export'
+ }
+})
+
+anim.activated.then(() => {
+ chart.feature.videoCapture.start()
+})
+
+anim.then(async (chart) => {
+ const output = await chart.feature.videoCapture.stop()
+ window.open(output.getObjectURL())
+})
+```
+
+## Extracting information
+
+The plugin provides two functions, start() to begin the recording and stop() to end it.
+
+```javascript
+chart.feature.videoCapture.start()
+chart.feature.videoCapture.stop()
+```
+
+## Contributing
+
+### Release
+
+If you need to change the `Vizzu` version number in the plugins, use the following command:
+
+`yarn node tools/updateVizzuMinorVersion.cjs `
+
+## License
+
+Copyright © 2021-2023 [Vizzu Inc.](https://vizzuhq.com)
+
+Released under the
+[Apache 2.0 License](https://lib.vizzuhq.com/latest/LICENSE/).
diff --git a/plugins/svg-render/build/build.js b/plugins/svg-render/build/build.js
new file mode 100644
index 0000000..c888dd0
--- /dev/null
+++ b/plugins/svg-render/build/build.js
@@ -0,0 +1,48 @@
+import * as esbuild from 'esbuild'
+import { polyfillNode } from 'esbuild-plugin-polyfill-node'
+import { copy } from 'esbuild-plugin-copy'
+
+await esbuild.build({
+ entryPoints: ['src/index.ts'],
+ bundle: true,
+ minify: true,
+ sourcemap: true,
+ platform: 'node',
+ format: 'esm',
+ plugins: [
+ polyfillNode({
+ globals: {
+ process: true,
+ Buffer: true
+ }
+ }),
+ copy({
+ resolveFrom: 'cwd',
+ assets: {
+ from: ['./src/*.d.ts'],
+ to: ['./dist/types']
+ },
+ watch: true
+ })
+ ],
+ outfile: 'dist/mjs/index.js'
+})
+
+await esbuild.build({
+ entryPoints: ['src/index.ts'],
+ bundle: true,
+ minify: true,
+ sourcemap: true,
+ platform: 'browser',
+ target: 'es6',
+ format: 'cjs',
+ plugins: [
+ polyfillNode({
+ globals: {
+ process: true,
+ Buffer: true
+ }
+ })
+ ],
+ outfile: 'dist/cjs/index.js'
+})
diff --git a/plugins/svg-render/build/clear.js b/plugins/svg-render/build/clear.js
new file mode 100644
index 0000000..cf12cc5
--- /dev/null
+++ b/plugins/svg-render/build/clear.js
@@ -0,0 +1,22 @@
+import fs from 'fs'
+
+function deleteFolderRecursive(path) {
+ if (fs.existsSync(path) && fs.lstatSync(path).isDirectory()) {
+ fs.readdirSync(path).forEach(function (file, index) {
+ var curPath = path + '/' + file
+
+ if (fs.lstatSync(curPath).isDirectory()) {
+ // recurse
+ deleteFolderRecursive(curPath)
+ } else {
+ // delete file
+ fs.unlinkSync(curPath)
+ }
+ })
+
+ fs.rmdirSync(path)
+ }
+}
+deleteFolderRecursive('./dist')
+
+fs.mkdirSync('./dist')
diff --git a/plugins/svg-render/demo/demo.js b/plugins/svg-render/demo/demo.js
new file mode 100644
index 0000000..650abc0
--- /dev/null
+++ b/plugins/svg-render/demo/demo.js
@@ -0,0 +1,34 @@
+import Vizzu from 'https://cdn.jsdelivr.net/npm/vizzu@0.11/dist/vizzu.min.js'
+import { data } from 'https://lib.vizzuhq.com/0.10/assets/data/chart_types_eu.js'
+import { VideoCapture } from '../dist/mjs/index.js'
+
+window.addEventListener('load', async function () {
+ const chart = new Vizzu('vizzu')
+
+ await chart.initializing
+
+ chart.feature(new VideoCapture(), true)
+
+ chart.animate({
+ data
+ })
+
+ const anim = chart.animate({
+ data,
+ config: {
+ x: 'Year',
+ y: ['Value 2 (+)', 'Joy factors'],
+ color: 'Joy factors',
+ title: 'Video Export'
+ }
+ })
+
+ anim.activated.then(() => {
+ chart.feature.videoCapture.start()
+ })
+
+ anim.then(async (chart) => {
+ const output = await chart.feature.videoCapture.stop()
+ window.open(output.getObjectURL())
+ })
+})
diff --git a/plugins/svg-render/demo/index.html b/plugins/svg-render/demo/index.html
new file mode 100644
index 0000000..404d047
--- /dev/null
+++ b/plugins/svg-render/demo/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Vizzu video maker demo
+
+
+
+
+
+
+
diff --git a/plugins/svg-render/demo/style.css b/plugins/svg-render/demo/style.css
new file mode 100644
index 0000000..aca3b4a
--- /dev/null
+++ b/plugins/svg-render/demo/style.css
@@ -0,0 +1,5 @@
+body {
+ font-family: Arial, sans-serif;
+ margin: 0;
+ padding: 0;
+}
diff --git a/plugins/svg-render/package.json b/plugins/svg-render/package.json
new file mode 100644
index 0000000..a1a4bbc
--- /dev/null
+++ b/plugins/svg-render/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "@vizzu/svg-render",
+ "description": "SVG rendering plugin",
+ "version": "0.2.0",
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./dist/types/index.d.ts",
+ "import": "./dist/mjs/index.js",
+ "default": "./dist/mjs/index.js",
+ "require": "./dist/cjs/index.js"
+ }
+ },
+ "files": [
+ "dist/**",
+ "package.json"
+ ],
+ "main": "dist/cjs/index.js",
+ "module": "dist/mjs/index.js",
+ "types": "dist/types/index.d.ts",
+ "scripts": {
+ "build": "run clear && node build/build.js",
+ "clear": "node build/clear.js",
+ "test": "vitest"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vizzuhq/vizzu-lib-ext#workspace=@vizzu/svg-render"
+ },
+ "keywords": [
+ "vizzu",
+ "plugin",
+ "svg"
+ ],
+ "email": "hello@vizzuhq.com",
+ "license": "Apache-2.0",
+ "author": "Vizzu Inc.",
+ "bugs": {
+ "url": "https://github.com/vizzuhq/vizzu-lib-ext/issues"
+ },
+ "homepage": "https://github.com/vizzuhq/vizzu-lib-ext/tree/main/plugins/svg-render#readme",
+ "devDependencies": {
+ "ts-standard": "^12.0.2",
+ "typescript": "^5.4.2",
+ "vitest": "^1.3.1"
+ },
+ "peerDependencies": {
+ "vizzu": "~0.10.1"
+ }
+}
diff --git a/plugins/svg-render/src/index.ts b/plugins/svg-render/src/index.ts
new file mode 100644
index 0000000..200d6a3
--- /dev/null
+++ b/plugins/svg-render/src/index.ts
@@ -0,0 +1,179 @@
+import { Vizzu, CRenderer, Plugin } from 'vizzu'
+
+class SvgRender extends CRenderer implements Plugin {
+ _svg = null
+ _states = []
+
+ meta = {
+ name: 'svgRender',
+ version: '0.10.1',
+ depends: ['htmlCanvas']
+ }
+
+ get hooks() {
+ const hooks = {
+ render: (ctx, next) => {
+ ctx.renderer = this
+ next()
+ }
+ }
+ return hooks
+ }
+
+ get _defState() {
+ return {
+ lineWidth: 1,
+ brushColor: 'rgba(0, 0, 0, 1)',
+ lineColor: 'rgba(0, 0, 0, 1)',
+ font: { size: 10, family: 'sans-serif' },
+ path: [],
+ gradient: null,
+ transforms: [{ a: 1, b: 0, c: 0, d: 1, e: 0.5, f: 0.5}],
+ clip: []
+ }
+ }
+
+ get _state() {
+ return this._states[this._states.length - 1]
+ }
+
+ save() {
+ this._states.push({
+ ...this._state,
+ transforms: [ ...this._state.transforms ],
+ clip: [ ...this._state.clip ]
+ })
+ }
+
+ restore() {
+ this._states.pop()
+ }
+
+ _transform(element) {
+ this._state.transforms.forEach((t) => element.transform(t))
+ }
+
+ _draw(element) {
+ element
+ .fill(this._state.gradient ?? this._state.brushColor)
+ .stroke({ width: this._state.lineWidth, color: this._state.lineColor })
+ this._transform(element)
+ this._state.clip.forEach((c) => {
+ element.clipWith(c)
+ })
+ }
+
+ _clip(element) {
+ this._transform(element)
+ this._state.clip.push(this._svg.clip().add(element))
+ }
+
+ frameBegin() {
+ this._states = [ this._defState ]
+ this._svg = SVG()
+ }
+
+ frameEnd() {
+// console.log(this._svg.svg())
+ const container = document.getElementById('drawing')
+ // remove all child of container
+ while (container.firstChild) {
+ container.removeChild(container.lastChild);
+ }
+ this._svg.addTo('#drawing').size(500, 350)
+ }
+
+ setClipRect(x, y, sizex, sizey) {
+ const rect = this._svg.rect(sizex, sizey).move(x, y)
+ this._clip(rect)
+ }
+
+ setClipCircle(x, y, radius) {
+ const circle = this._svg.circle(radius * 2).move(x - radius, y - radius)
+ this._clip(circle)
+ }
+
+ setClipPolygon() {
+ this._state.path.push(['z'])
+ const path = this._svg.path(this._state.path)
+ this._clip(path)
+ }
+
+ setBrushColor(r, g, b, a) {
+ this._state.gradient = null
+ this._state.brushColor = 'rgba(' + r * 255 + ',' + g * 255 + ',' + b * 255 + ',' + a + ')'
+ }
+
+ setLineColor(r, g, b, a) {
+ this._state.lineColor = 'rgba(' + r * 255 + ',' + g * 255 + ',' + b * 255 + ',' + a + ')'
+ }
+
+ setLineWidth(width) {
+ this._state.lineWidth = width
+ }
+
+ setFontStyle(font) {
+ const size = font.match(/([0-9.]+)px/)[1]
+ const family = font.match(/([0-9.]+)px (.+),/)[2]
+ this._state.font = {
+ size,
+ family,
+ }
+ }
+
+ beginDropShadow() {}
+ setDropShadowBlur(radius) {}
+ setDropShadowColor(r, g, b, a) {}
+ setDropShadowOffset(x, y) {}
+ endDropShadow() {}
+
+ beginPolygon() {
+ this._state.path = []
+ }
+
+ addPoint(x, y) {
+ this._state.path.push([this._state.path.length === 0 ? 'M' : 'L', x, y])
+ }
+
+ addBezier(c0x, c0y, c1x, c1y, x, y) {
+ this._state.path.push(['C', c0x, c0y, c1x, c1y, x, y ])
+ }
+
+ endPolygon() {
+ this._state.path.push(['z'])
+ this._draw(this._svg.path(this._state.path))
+ }
+
+ rectangle(x, y, sizex, sizey) {
+ this._draw(this._svg.rect(sizex, sizey).move(x, y))
+ }
+
+ circle(x, y, radius) {
+ this._draw(this._svg.circle(radius * 2).move(x - radius, y - radius))
+ }
+
+ line(x1, y1, x2, y2) {
+ this._draw(this._svg.line(x1, y1, x2, y2))
+ }
+
+ drawText(x, y, sizex, sizey, text) {
+ const x2 = x + (sizex < 0 ? -sizex : 0)
+ const y2 = y + (sizey < 0 ? -sizey : 0)
+ const element = this._svg.text(text).font(this._state.font)
+ .attr('dominant-baseline','hanging')
+ this._draw(element)
+ }
+
+ setGradient(x1, y1, x2, y2, gradient) {
+ this._state.gradient = this._svg.gradient('linear', (add) => {
+ gradient.stops.forEach((g) => add.stop(g.offset, g.color))
+ })
+ this._gradient.from(x1, y1).to(x2, y2)
+ }
+
+ transform(a, b, c, d, e, f) {
+ this._state.transforms.push({ a, b, c, d, e, f })
+ }
+}
+
+export default SvgRender
diff --git a/plugins/svg-render/tests/init.test.js b/plugins/svg-render/tests/init.test.js
new file mode 100644
index 0000000..4075b3a
--- /dev/null
+++ b/plugins/svg-render/tests/init.test.js
@@ -0,0 +1,13 @@
+import { expect, test, describe } from 'vitest'
+import { VideoCapture } from '../src'
+
+describe('init', () => {
+ const videoCapture = new VideoCapture()
+ test('video capture initialized', () => {
+ expect(typeof videoCapture).toBe('object')
+ const functionKeys = Object.keys(videoCapture)
+ const defultKeys = ['meta', 'options']
+
+ expect(functionKeys).toEqual(expect.arrayContaining(defultKeys))
+ })
+})
diff --git a/yarn.lock b/yarn.lock
index 47c4bc5..c1a896b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1434,6 +1434,18 @@ __metadata:
languageName: node
linkType: hard
+"@vizzu/svg-render@workspace:plugins/svg-render":
+ version: 0.0.0-use.local
+ resolution: "@vizzu/svg-render@workspace:plugins/svg-render"
+ dependencies:
+ ts-standard: "npm:^12.0.2"
+ typescript: "npm:^5.4.2"
+ vitest: "npm:^1.3.1"
+ peerDependencies:
+ vizzu: ~0.10.1
+ languageName: unknown
+ linkType: soft
+
"@vizzu/video-capture@workspace:plugins/video-capture":
version: 0.0.0-use.local
resolution: "@vizzu/video-capture@workspace:plugins/video-capture"