{"id":18653372,"url":"https://github.com/alexandre-fernandez/pathcrafter","last_synced_at":"2025-04-11T16:32:29.157Z","repository":{"id":261444131,"uuid":"640998442","full_name":"Alexandre-Fernandez/pathcrafter","owner":"Alexandre-Fernandez","description":"Create responsive document-relative SVG paths programmatically.","archived":false,"fork":false,"pushed_at":"2023-10-20T07:30:37.000Z","size":551,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-11-06T16:17:30.813Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/pathcrafter","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Alexandre-Fernandez.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-05-15T15:01:49.000Z","updated_at":"2024-09-18T11:28:23.000Z","dependencies_parsed_at":"2024-11-06T16:27:34.609Z","dependency_job_id":null,"html_url":"https://github.com/Alexandre-Fernandez/pathcrafter","commit_stats":null,"previous_names":["alexandre-fernandez/pathcrafter"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Alexandre-Fernandez%2Fpathcrafter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Alexandre-Fernandez%2Fpathcrafter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Alexandre-Fernandez%2Fpathcrafter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Alexandre-Fernandez%2Fpathcrafter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Alexandre-Fernandez","download_url":"https://codeload.github.com/Alexandre-Fernandez/pathcrafter/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223472762,"owners_count":17150745,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-07T07:11:21.734Z","updated_at":"2024-11-07T07:11:22.242Z","avatar_url":"https://github.com/Alexandre-Fernandez.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\" \u003e\n    \u003cimg src=\"https://github.com/Alexandre-Fernandez/pathcrafter/blob/main/img/logo.png\" alt=\"pathcrafter logo\" width=\"66%\"\u003e\n    \u003cbr/\u003e\n\t\u003ch1\u003epathcrafter\u003c/h1\u003e\n    \u003cp\u003eCreate responsive document-relative SVG paths programmatically.\u003c/p\u003e\n\u003c/div\u003e\n\n## Features\n\n-   🪶 Lightweight\n-   🔧 Dynamic SVG paths\n-   🕊️ DOM-indepedent\n-   🔒 Type-safety and autocompletion\n\n## Introduction\n\nWith pathcrafter you can easily create absolute positionned (relative to the document root) SVG paths programmatically. To compensate its absolute positionning, pathcrafter provides many utilities to maintain the ability to work with DOM elements.\nThis unlocks many possibilities such as making a responsive line between two elements in totally different containers.\n\n## Installation\n\n```yml\n# npm\nnpm install pathcrafter\n# yarn\nyarn add pathcrafter\n# pnpm\npnpm add pathcrafter\n```\n\n## Get started\n\nLet's create a SVG path, for that we will need a starting point, don't forget, **all pathcrafter coordinates are global** (relative to the document root) and **the Y axis is reversed** (top is negative, bottom is positive).\nBy default generated paths are stroke-only.\n\n```ts\nimport { Path } from \"pathcrafter\"\n\n// defines a path that starts at the document's top-left :\nnew Path({ x: 0, y: 0 })\n// you can also use a getter :\nnew Path(() =\u003e ({ x: 0, y: 0 }))\n```\n\nIf we want to position the starting point relative to an element we can use the getter syntax and `getElementEdgePoint`.\n\n```ts\nimport { Path, getElementEdgePoint } from \"pathcrafter\"\n\nconst div1 = document.querySelector(\"#div1\")\n\n// this path will start on div1's bottom edge at exactly 25% from its left :\nnew Path(() =\u003e getElementEdgePoint(div1, \"bottom\", 25))\n// we can also make it start 10 pixel below the same point :\nnew Path(() =\u003e {\n\tconst { x, y } = getElementEdgePoint(div1, \"bottom\", 25)\n\treturn {\n\t\tx,\n\t\ty: y + 10,\n\t}\n})\n```\n\nThere's all sort of utilities to position your paths relatively to DOM elements.\nNow that we know how to add a starting point we can see how to add segments to a Path. Be aware that **paths segments will always start where you left before** and that **you cannot remove a segment once you add it**.\n\n```ts\nimport { Path, getElementEdgePoint } from \"pathcrafter\"\n\nconst div1 = document.querySelector(\"#div1\")\n\nnew Path(() =\u003e getElementEdgePoint(div1, \"bottom\", 25))\n\t.addVertical(50) // adding a vertical segment that goes 50px in the bottom direction\n\t.addDiagonal(() =\u003e ({ x: 60, y: 20 })) // diagonal segment that goes 60px to the left and 20px down\n```\n\nLet's link two elements together.\n\n```ts\nimport { Path, getElementEdgePoint, getDistance } from \"pathcrafter\"\n\nconst div1 = document.querySelector(\"#div1\")\nconst div2 = document.querySelector(\"#div2\")\n\nnew Path(() =\u003e getElementEdgePoint(div1, \"right\", 0))\n\t// getters receive the last position as their first parameter, this will be where\n\t// the previous segment left off, or the starting point if it's the first segment\n\t.addDiagonal((lastPosition) =\u003e {\n\t\tconst endingPoint = getElementEdgePoint(div2, \"left\", 100)\n\t\treturn getDistance(lastPosition, endingPoint)\n\t})\n```\n\nOnce we have our path we just have to plug it into the `pathcrafter` function. So that it can be rendered on the DOM. If we want it to be responsive we can use the `update` callback function to rerun all the getters, whenever we need to.\nFor that we can use `addEventListener`, `MutationObserver`, etc...\n\n```ts\nimport {\n\tPath,\n\tpathcrafter,\n\tgetElementEdgePoint,\n\tgetDistance,\n} from \"pathcrafter\"\n\nconst div1 = document.querySelector(\"#div1\")\nconst div2 = document.querySelector(\"#div2\")\n\nconst pathBetweenDiv1AndDiv2 = new Path(() =\u003e\n\tgetElementEdgePoint(div1, \"right\", 0),\n).addDiagonal((lastPosition) =\u003e {\n\tconst endingPoint = getElementEdgePoint(div2, \"left\", 100)\n\treturn getDistance(lastPosition, endingPoint)\n})\n\nconst { update } = pathcrafter([pathBetweenDiv1AndDiv2]) // array of paths\nwindow.addEventListener(\"resize\", update) // updating path on window resize\n```\n\nYou can give multiple paths to the `pathcrafter` function, however **all grouped paths will have the same stroke width**. If you need different stroke widths you can use `pathcrafter` multiple times for each stroke width.\n\n## Reference\n\n### `Path`\n\n`Path` is the class used to create SVG paths.\n\n```ts\nclass Path {\n\t/** Read-only id of this `Path` object and of the DOM element. */\n\tid: string = generateUniqueId()\n\n\tfill: string = \"none\"\n\n\tstroke: string = \"black\"\n\n\tconstructor(\n\t\tstartingPoint: Coordinates2d | Coordinates2dGetter,\n\t\toptions?: Partial\u003cPathOptions\u003e = {},\n\t) {}\n\n\t/** Adds a horizontal movement, negative is left, positive is right. */\n\taddHorizontal(length: number | LengthGetter, marker?: string): this {}\n\n\t/** Adds a vertical movement, negative is up, positive is down. */\n\taddVertical(length: number | LengthGetter, marker?: string): this {}\n\n\t/** Adds a diagonal movement, the Y-axis is reversed (negative is up). */\n\taddDiagonal(\n\t\tlength: Coordinates2d | Coordinates2dGetter,\n\t\tmarker?: string,\n\t): this {}\n\n\t/** Adds a cubic bezier movement, the Y-axis is reversed (negative is up). */\n\taddCubic(\n\t\tlength: Coordinates2d | Coordinates2dGetter,\n\t\tstartControl: Coordinates2d | Coordinates2dGetter,\n\t\tendControl: Coordinates2d | Coordinates2dGetter,\n\t\tmarker?: string,\n\t): this {}\n\n\t/** Adds a quadratic movement, the Y-axis is reversed (negative is up). */\n\taddQuadratic(\n\t\tlength: Coordinates2d | Coordinates2dGetter,\n\t\tcontrol: Coordinates2d | Coordinates2dGetter,\n\t\tmarker?: string,\n\t): this {}\n\n\t/**\n\t * **Only use if you know what you're doing.**\n\t *\n\t * Clears this path's cache.\n\t * This will make the movement functions rerun when the path is updated.\n\t * The cache should only be cleared when this and all derived paths have\n\t * been updated.\n\t */\n\tclearCache(): this {}\n\n\t/** Returns the DOM element corresponding to this object. */\n\tgetElement(): Element {}\n\n\t/** Updates the DOM element's attributes. */\n\tupdateElement(): this {}\n\n\t/**\n\t * Derives a parallel path from this one.\n\t * @param gap The positive or negative gap describes the gap between this\n\t * path and the derived one.\n\t */\n\tderiveParallel(gap: number, options?: Partial\u003cPathOptions\u003e): Path {}\n\n\t/**\n\t * Derives a parallel path from this one.\n\t * @param marker The marker will look for markers on a movement and derive a\n\t * new path from this one including everything up to that point.\n\t */\n\tderivePartial(marker: string, options?: Partial\u003cPathOptions\u003e): Path {}\n\n\t/** Derives an identical path from this one. */\n\tderive(options?: Partial\u003cPathOptions\u003e): Path {}\n}\n```\n\nThe `Path` constructor takes the following options.\n\n```ts\ninterface PathOptions {\n\tid: string\n\tfill: string\n\tstroke: string\n}\n```\n\nDerived paths reference the parent path movements and can modify their return values before applying them.\nEach movement getter should (and will if used with `pathcrafter`) only run once per Path (including derived paths) per update.\n\n### `pathcrafter`\n\nThe `pathcrafter` function manages your created `Path`, it displays and updates them when needed.\n\n```ts\nfunction pathcrafter(\n\tpaths: Path[],\n\toptions: Partial\u003cPathcrafterOptions\u003e = {},\n): { update: () =\u003e void } {}\n```\n\nThe `pathcrafter` function takes the following options.\n\n```ts\ninterface PathcrafterOptions {\n\tid: string\n\tstrokeWidth: number | string\n}\n```\n\n### `getElementRect`\n\nThe `getElementRect` function returns an elements rectangle relative to the `Document`.\n\n```ts\nfunction getElementRect(element: SelectorElement): Rect2d {}\n```\n\n### `getElementEdgePoint`\n\nThe `getElementEdgePoint` function returns a point from the edge of an element's rectangle. `edge` can be `\"top\"`, `\"bottom\"`, `\"right\"` or `\"left\"`, and `percentage` refers to the distance in % from the top of the edge for vertical edges or from the left of the edge for horizontal edges.\n\n```ts\nfunction getElementEdgePoint(\n\telement: SelectorElement,\n\tedge: Direction,\n\tpercentage: number,\n): Point2d {}\n```\n\n### `getDistance`\n\nThe `getDistance` function simply performs a substraction between `destination` and `position` giving you the distance between the two points. If `position` and `destination` are numbers then it will return a number otherwise if `position` and `destination` are `Coordinates2d` (`{ x: number, y: number }`) it will return a `Coordinates2d`.\n\n```ts\nfunction getDistance\u003cT extends number | Coordinates2d\u003e(\n\tposition: T,\n\tdestination: T,\n): T extends number ? number : Coordinates2d {}\n```\n\n### `getGapRect`\n\nThe `getGapRect` function return a rectangle corresponding to a gap between two elements. If the elements intersect it will return `null`.\n\n```ts\nfunction getGapRect(\n\telement1: SelectorElement,\n\telement2: SelectorElement,\n): Rect2d | null {}\n```\n\n### `getGapX`\n\nThe `getGapX` function return a `percentage` of the horizontal gap between two elements. This can be useful if you want to position your path between two element.\n\n```ts\nfunction getGapX(\n\telement1: SelectorElement,\n\telement2: SelectorElement,\n\tpercentage = 100,\n): number {}\n```\n\n### `getGapY`\n\nThe `getGapY` function return a `percentage` of the vertical gap between two elements. This can be useful if you want to position your path between two element.\n\n```ts\nfunction getGapY(\n\telement1: SelectorElement,\n\telement2: SelectorElement,\n\tpercentage = 100,\n): number {}\n```\n\n### `createPoint2d`\n\nCreates a `Point2d` from a X and Y. You can use this function to create 2D coordinates easily.\n\n```ts\nfunction createPoint2d(x: number, y: number): Point2d {}\n```\n\n```ts\nclass Point2d implements Coordinates2d {\n\tx: number\n\n\ty: number\n\n\tadd({ x, y }: Point2d): this {}\n\n\tequals({ x, y }: Point2d): boolean {}\n\n\tclone(): Point2d {}\n\n\tvalues(): [number, number] {}\n\n\ttoString(): string {}\n}\n```\n\n### `createRect2d`\n\nCreates a `Rect2d` from the top left (`position`) and the bottom right (`end`) corner. You can use this function to represent 2D rectangles.\n\n```ts\nfunction createRect2d(position: Coordinates2d, end: Coordinates2d): Rect2d {}\n```\n\n```ts\nclass Rect2d {\n\tend: Point2d\n\n\tposition: Point2d\n\n\tarea: number\n\n\twidth: number\n\n\theight: number\n\n\ttop: number\n\n\tbottom: number\n\n\tleft: number\n\n\tright: number\n\n\tconstructor(\n\t\tend: Coordinates2d,\n\t\tposition: Coordinates2d = new Point2d(0, 0),\n\t) {}\n\n\tgetIntersection(rect: Rect2d): Rect2d | null {}\n\n\tgetGap(rect: Rect2d, returnIntersection = false): Rect2d | null {}\n\n\ttoString(): string {}\n}\n```\n\n### `createVector2d`\n\nCreates a `Vector2d` from the origin (`tail`) and destination (`head`) coordinates. You can use this function to represent 2D Vectors/Segments.\n\n```ts\nfunction createVector2d(tail: Coordinates2d, head: Coordinates2d): Vector2d {}\n```\n\n```ts\nclass Vector2d {\n\thead: Point2d\n\n\ttail: Point2d\n\n\tconstructor(head: Coordinates2d, tail: Coordinates2d = new Point2d(0, 0)) {}\n\n\ttranslate(x: number, y: number): this {}\n\n\tperpendicularTranslate(length: number): this {}\n\n\tlength(): number {}\n\n\tadd({ head, tail }: Vector2d): this {}\n\n\tsubstract({ head, tail }: Vector2d): this {}\n\n\tequals({ head, tail }: Vector2d): boolean {}\n\n\tabs(): this {}\n\n\tnormalize(): this {}\n\n\tscalarDivide(scalar: number): this {}\n\n\tscalarMultiply(scalar: number): this {}\n\n\tisPositionVector(): boolean {\n\t\treturn this.tail.x === 0 \u0026\u0026 this.tail.y === 0\n\t}\n\n\tclone(): Vector2d {\n\t\treturn Vector2d.fromCoordinates(\n\t\t\tthis.head.x,\n\t\t\tthis.head.y,\n\t\t\tthis.tail.x,\n\t\t\tthis.tail.y,\n\t\t)\n\t}\n\n\ttoLine2d(): Line2d {}\n\n\ttoString(): string {}\n}\n```\n\n### `getDocumentSize`\n\n`getDocumentSize` returns the current size of the document.\n\n```ts\nfunction getDocumentSize(): { width: number; height: number } {}\n```\n\n### `getBoundingDocumentRect`\n\n`getBoundingDocumentRect` returns an element's `DOMRect` relative to the whole `Document` in contrast to the native `getBoundingClientRect` which returns the `DOMRect` relative to the current view.\n\n```ts\nfunction getBoundingDocumentRect(element: Element): DOMRect {}\n```\n\n## Contributors\n\n\u003ctable\u003e\n\t\u003ctbody\u003e\n\t\t\u003ctr\u003e\n\t\t\t\u003ctd align=\"center\"\u003e\n\t\t\t\t\u003ca href=\"https://github.com/Alexandre-Fernandez\"\u003e\n\t\t\t\t\t\u003cfigure\u003e\n\t\t\t\t\t\t\u003cimg src=\"https://avatars.githubusercontent.com/u/79476242?v=4?s=100\" width=\"100px;\"\n\t\t\t\t\t\t\talt=\"Alexandre Fernandez\"\u003e\n\t\t\t\t\t\t\u003cbr /\u003e\n\t\t\t\t\t\t\u003cfigcaption\u003e\u003csub\u003eAlexandre Fernandez\u003c/sub\u003e\u003c/figcaption\u003e\n\t\t\t\t\t\u003c/figure\u003e\n\t\t\t\t\u003c/a\u003e\n\t\t\t\u003c/td\u003e\n\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexandre-fernandez%2Fpathcrafter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexandre-fernandez%2Fpathcrafter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexandre-fernandez%2Fpathcrafter/lists"}