Skip to content

Commit

Permalink
feat: Basic elements (#23)
Browse files Browse the repository at this point in the history
* feat(parser): initial work (in progress)

* test: add dumb html file to test parser

* chore: setup ts

* chore: ignore dist folder

* fix(dev): fix dev html docs

Add additional ESM build so that dist build may be used directly in dev html docs.

* docs: add a few dev docs

Adding a couple documentions in the the dev folder to help keep track of notes and such.

* feat(utils): add cubic bezier points resolution

* feat(utils): add path to points converter (wip)

* feat(bezier): enhance cubic bézier curve generation

- add docs
- update name of function to get points
- refactor precision argument to be the number of points to return (without origin point)
- use Number constructor rather than implicit type coercion (+)

* fix(path-to-points): return type

* fix(path-to-points): imported function name from bézier util

* docs(path-to-points): add function description

* fix(path-to-points): return elements

* docs(parser): remove useless jsdoc

* refactor(parser): do not export utils functions

* feat(parser): wip

* feat(types): setup basic types

* chore(scripts): update

- Remove "--" to forward options
- Remove redundant lint scripts

* chore(deps): update dependencies

* fix(lint): remove prettier typescript eslint config

See https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md#version-800-2021-02-21

* fix(path-to-points): do not expand points

* style: arrow fn everywhere

* chore: missing lockfile

* style: double quotes everywhere

* fix(parser): position handling of multiple elements

In the main scene, and in their boundaries

* feat(path): set strokeSharpness

* feat(dev): output results to textarea

* feat(parser): return valid Excalidraw elements

Also update types and variables/functios names for consistency

* feat(parser): add backgroundColor

* style: run prettier

* chore(scripts): apply prettier to js and ts files

* feat(parser): return a valid Excalidraw file content

* feat(dev): update style and behaviour

* style: fix lint

* fix: unused import

* feat(path-to-points): on close path, make sure last committed point is first point

* feat(path-to-points): if no close path and only one element, make sure to publish points

* refactor(path-to-points): simplify commands regex

* feat(bezier): add support for quadratic bézier curves

* feat(path-to-points): add support for quadratic bézier curves

* style: fix lint

* ci: add workflow to cancel previous runs

* feat(path-to-points): add support for simplified quadratic bézier curves

* feat(path-to-points): add support for simplified cubic bézier curves

* style(path-to-points): lint

* feat(path-to-points): enhance commands regex

* feat(path-to-points): more debugging

* refactor: rename everything from coordinate(s) to point(s)

- ensure consistency with excalidraw elements defining an array of points
- also removes useless Coordinates type as it is no more than an array of number

* refactor(path-to-points): use handling functions for each command

* feat(path-parser): handling of colors

* refactor: rename EVERYTHING to point(s)

* create method for "walking" elements of an svg tree. Start working on implementing serialization into excalidraw json of a few basic element types.

* chore: setup @excalidraw/eslint-config

* style: run eslint

* chore(devDeps): update dev dependencies

* chore: fix eslint-plugin-prettier version range

* fix(path-to-points): allow parameters omitting leading 0 (.75)

Also remove duplicated non-capturing groups

* feat(path): add utility to work with ellipses

* feat(path-to-points): add support for arcTo commands

* style: lint + todos

* chore(devDeps): update dependencies

- also remove useless eslint-plugin-import

* chore: use @excalidraw/eslint-config

* chore: remove some package fields

* Create svg dom walker to support further svg elements. Add basic handling of presentation and filter attributes. Start work on handling transform attributes.

* Add back main and files entries into package.json.

* refactor attributes handlers.

* Fix issues with use element attributes.

* Fix some issues with placing circles.

* Updates to circle and ellipse elements.

* Refinement work on rect elements.

* Add first pass at polyline and polygon element rendering.

* Add back main and files to package.json. Remove unused file.

* Add back a number of missing deps

* Fix lint errors

* Add chroma.js types, fix type error in path walker.

* update yarn.lock

Co-authored-by: Nicolas Goudry <goudry.nicolas@gmail.com>
Co-authored-by: Nicolas <nicolas@plum-energie.com>
  • Loading branch information
3 people authored Mar 17, 2021
1 parent cacada5 commit 025aa73
Show file tree
Hide file tree
Showing 24 changed files with 2,026 additions and 3 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/cancel.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Cancel previous runs

on:
push:
branches:
- master
pull_request:

jobs:
cancel:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: styfle/cancel-workflow-action@0.6.0
with:
workflow_id: 6045716, 6046665
access_token: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*.tgz
logs
node_modules
dist
npm-debug.log*
package-lock.json
static
Expand Down
118 changes: 118 additions & 0 deletions dev/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html>
<head>
<title>svg-to-excalidraw</title>
<style>
html {
height: 100%;
width: 100%;
font-family: Verdana, Geneva, sans-serif;
}

body {
display: flex;
flex-direction: column;
align-items: stretch;
margin: 0;
height: 100%;
}

header {
text-align: center;
font-size: 1.6rem;
text-transform: uppercase;
background-color: cornflowerblue;
color: cornsilk;
}

header > h1 {
padding: 0.5rem;
}

main {
display: flex;
flex-direction: row;
height: 100%;
}

main > aside {
display: flex;
flex-direction: column;
align-content: center;
flex: 1;
background-color: rgba(100, 149, 237, 0.3);
padding: 0.8rem;
}

main > textarea {
flex: 3;
}

.custom-file-upload {
display: inline-block;
cursor: pointer;
margin: 1rem 0;
border-radius: 0.2rem;
background-color: #fff;
border: 1px solid #333;
color: #333;
padding: 0.5rem;
}

#source {
display: none;
}

#result {
margin: 1rem;
padding: 0.5rem;
}
</style>
</head>
<body>
<header><h1>svg-to-excalidraw</h1></header>
<main>
<aside>
<div>File to convert :</div>
<label for="source" class="custom-file-upload">Pick a SVG file</label>
<input type="file" id="source" accept=".svg" />
</aside>
<textarea
id="result"
placeholder="Result in excalidraw file format should be output here"
></textarea>
</main>
<script type="module">
import svgParse from "../dist/esm-bundle.js";

const fileSelector = document.getElementById("source");
const output = document.getElementById("result");

output.addEventListener("click", function (event) {
this.select();
});

fileSelector.addEventListener("change", (event) => {
const fileList = event.target.files;
readFile(fileList[0]);
});

function readFile(file) {
if (file.type && file.type !== "image/svg+xml") {
console.log("File is not SVG.");

return;
}

const reader = new FileReader();

reader.readAsText(file);
reader.addEventListener("load", (event) => {
const parsingResult = svgParse.parse(event.target.result);

output.value = JSON.stringify(parsingResult, null, 2);
});
}
</script>
</body>
</html>
11 changes: 11 additions & 0 deletions dev/notes/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# TODO

Note: this can probably go away once we are able to get PRs merged upstream.

### Test

[ ] Add Jest library

### Code Quality

[ ] Add Prettier
5 changes: 5 additions & 0 deletions dev/notes/excalidraw-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Excalidraw Notes

### Elements

[Element types](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts)
5 changes: 5 additions & 0 deletions dev/notes/svg-parsing-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SVG Parsing Notes

[MDN SVG Element reference](https://developer.mozilla.org/en-US/docs/Web/SVG/Element)

[MDN SVG Attribute reference](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute)
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
"name": "svg-to-excalidraw",
"version": "0.0.0",
"description": "Convert SVG to Excalidraw’s file format",
"main": "dist/bundle.js",
"files": [
"dist/bundle.js",
"svg-to-excalidraw.d.ts"
],
"scripts": {
"build": "yarn clean && webpack --config webpack.config.js",
"build:watch": "yarn clean && webpack --config webpack.config.js --watch",
Expand Down Expand Up @@ -31,8 +36,10 @@
"@babel/preset-typescript": "7.13.0",
"@excalidraw/eslint-config": "1.0.1",
"@excalidraw/prettier-config": "1.0.2",
"@types/chroma-js": "^2.1.3",
"@typescript-eslint/eslint-plugin": "4.17.0",
"@typescript-eslint/parser": "4.17.0",
"babel-loader": "^8.2.2",
"eslint": "7.21.0",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-prettier": "3.3.1",
Expand All @@ -47,5 +54,11 @@
"lint-staged": {
"*.js": "eslint --cache --fix",
"*.{js,css,md}": "prettier --write"
},
"dependencies": {
"chroma-js": "^2.1.1",
"gl-matrix": "^3.3.0",
"nanoid": "^3.1.20",
"roughjs": "^4.3.1"
}
}
133 changes: 133 additions & 0 deletions src/attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import chroma from "chroma-js";
import { ExcalidrawElementBase } from "./elements/ExcalidrawElement";

export function hexWithAlpha(color: string, alpha: number): string {
return chroma(color).alpha(alpha).css();
}

export function has(el: Element, attr: string): boolean {
return el.hasAttribute(attr);
}

export function get(el: Element, attr: string, backup?: string): string {
return el.getAttribute(attr) || backup || "";
}

export function getNum(el: Element, attr: string, backup?: number): number {
const numVal = Number(get(el, attr));
return numVal === NaN ? backup || 0 : numVal;
}

const presAttrs = {
stroke: "stroke",
"stroke-opacity": "stroke-opacity",
"stroke-width": "stroke-width",
fill: "fill",
"fill-opacity": "fill-opacity",
opacity: "opacity",
} as const;

type ExPartialElement = Partial<ExcalidrawElementBase>;

type AttrHandlerArgs = {
el: Element;
exVals: ExPartialElement;
};

type PresAttrHandlers = {
[key in keyof typeof presAttrs]: (args: AttrHandlerArgs) => void;
};

const attrHandlers: PresAttrHandlers = {
stroke: ({ el, exVals }) => {
const strokeColor = get(el, "stroke");

exVals.strokeColor = has(el, "stroke-opacity")
? hexWithAlpha(strokeColor, getNum(el, "stroke-opacity"))
: strokeColor;
},

"stroke-opacity": ({ el, exVals }) => {
exVals.strokeColor = hexWithAlpha(
get(el, "stroke", "#000000"),
getNum(el, "stroke-opacity"),
);
},

"stroke-width": ({ el, exVals }) => {
exVals.strokeWidth = getNum(el, "stroke-width");
},

fill: ({ el, exVals }) => {
const fill = get(el, `fill`);

exVals.backgroundColor = fill === "none" ? "#00000000" : fill;
},

"fill-opacity": ({ el, exVals }) => {
exVals.backgroundColor = hexWithAlpha(
get(el, "fill", "#000000"),
getNum(el, "fill-opacity"),
);
},

opacity: ({ el, exVals }) => {
exVals.opacity = getNum(el, "opacity", 100);
},
};

// Presentation Attributes for SVG Elements:
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/Presentation
export function presAttrsToElementValues(
el: Element,
): Partial<ExcalidrawElementBase> {
const exVals = [...el.attributes].reduce((exVals, attr) => {
const name = attr.name;

if (Object.keys(attrHandlers).includes(name)) {
attrHandlers[name as keyof PresAttrHandlers]({ el, exVals });
}

return exVals;
}, {} as ExPartialElement);

return exVals;
}

type FilterAttrs = Partial<
Pick<ExcalidrawElementBase, "x" | "y" | "width" | "height">
>;

export function filterAttrsToElementValues(el: Element): FilterAttrs {
const filterVals: FilterAttrs = {};

if (has(el, "x")) {
filterVals.x = getNum(el, "x");
}

if (has(el, "y")) {
filterVals.y = getNum(el, "y");
}

if (has(el, "width")) {
filterVals.width = getNum(el, "width");
}

if (has(el, "height")) {
filterVals.height = getNum(el, "height");
}

return filterVals;
}

export function pointsAttrToPoints(el: Element): number[][] {
let points: number[][] = [];

if (has(el, "points")) {
points = get(el, "points")
.split(" ")
.map((p) => p.split(",").map(parseFloat));
}

return points;
}
Loading

0 comments on commit 025aa73

Please sign in to comment.