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"