{"id":41257114,"url":"https://github.com/andrewcourtice/ripl","last_synced_at":"2026-01-23T01:53:01.195Z","repository":{"id":98889952,"uuid":"508504070","full_name":"andrewcourtice/ripl","owner":"andrewcourtice","description":"Ripl provides a unified API for 2D graphics rendering in the browser with a focus towards high performance and interactive data visualization.","archived":false,"fork":false,"pushed_at":"2026-01-16T09:01:00.000Z","size":7255,"stargazers_count":317,"open_issues_count":0,"forks_count":6,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-01-16T23:35:54.199Z","etag":null,"topics":["canvas","javascript","typescript"],"latest_commit_sha":null,"homepage":"https://ripl.rocks","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andrewcourtice.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2022-06-29T01:17:32.000Z","updated_at":"2026-01-16T09:01:04.000Z","dependencies_parsed_at":"2023-11-24T07:28:04.443Z","dependency_job_id":"e6e26b85-eab6-4c2d-9e91-2d5fce0e923e","html_url":"https://github.com/andrewcourtice/ripl","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/andrewcourtice/ripl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrewcourtice%2Fripl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrewcourtice%2Fripl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrewcourtice%2Fripl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrewcourtice%2Fripl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andrewcourtice","download_url":"https://codeload.github.com/andrewcourtice/ripl/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrewcourtice%2Fripl/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28677714,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-23T01:00:35.747Z","status":"ssl_error","status_checked_at":"2026-01-23T01:00:19.529Z","response_time":144,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["canvas","javascript","typescript"],"created_at":"2026-01-23T01:53:00.668Z","updated_at":"2026-01-23T01:53:01.187Z","avatar_url":"https://github.com/andrewcourtice.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Ripl\n\nRipl (pronounced ripple) is a library that provides a unified API for 2D graphics rendering (canvas \u0026 SVG) in the browser with a focus towards high performance and interactive data visualization.\n\nWorking with the canvas API can be notoriously difficult as it is designed to be very low-level. Alternatively, working with SVG is rather straightforward but not without its flaws. Because these paradigms differ widely in their implementations developers often have to choose one or the other at the outset of a project. Ripl alleviates the issue of choosing between these mediums by exposing a unified API and mimicking the DOM/CSSOM in as many ways possible to make it simple for developers to interact with. Switching between Canvas and SVG is as simple as changing 1 line of code.\n\nRipl also exposes a number of methods such as scales, geometry, interpolation, color, data joining and easing to assist with drawing (inspired by D3).\n\n## Features\n\n- Unified API for drawing to different contexts\n- Grouping and property inheritance\n- Scene and renderer management\n- Event bubbling, delegation and stop propagation (mimics the DOM as much as possible. Capture phase only)\n- Powerful element querying (`getElementById`, `getElementsByType`, `getElementsByClass`) including CSS selector style querying via `query` and `queryAll`\n- Element bounds detection with `getBoundingBox`\n- Automatic interpolation for known property types (ie. can interpolate numbers, points, colors between RGB and Hex etc.)\n- Point extrapolation for shape morphing (currently only supports regular polygons)\n- High performance async animation including CSS-like keyframe animation support and custom interpolators\n- Several built-in shape primitives such as arc, circle, rect, line, polyline, text, ellipse, and polygon\n- Hoisted scenegraph to optimize render performance when using scenes. See [performance](#performance)\n- Completely modular and tree-shakable. Only ship the features that you use.\n- Strictly typed in TypeScript\n- Zero runtime dependencies\n\nCurrently unsupported features are: gradients, transforms, clipping.\n\n\u003e [!IMPORTANT]\n\u003e Ripl is currently in the early stages of development and, as a result, not yet published to NPM.\n\n## Example\n\nHere are a few proof-of-concept data-visualization examples created using Ripl:\n\n### Multi-Series Trend Chart (Bar/Line)\n\n- [Demo](https://www.ripl.rocks/docs/charts/trend.html)\n- [Source](https://github.com/andrewcourtice/ripl/blob/main/packages/charts/src/charts/trend.ts)\n  \n  ![trend 1](https://github.com/andrewcourtice/ripl/assets/11718453/797ed163-614d-4783-998c-383070c7edf5)\n\n### Donut Chart with Hover Effects\n\n- [Demo](https://www.ripl.rocks/docs/charts/pie.html)\n- [Source](https://github.com/andrewcourtice/ripl/blob/main/packages/charts/src/charts/pie.ts)\n  \n  ![donut](https://github.com/andrewcourtice/ripl/assets/11718453/15a38877-48cc-462c-9d57-8227afa5f7d0)\n\n## Usage\n\nThe following is a tour of Ripl's features starting from the most basic and progressively building towards more advanced concepts.\n\n### Render a Basic Element\n\nHere's a basic example of rendering an element.\n\n```typescript\nimport {\n    createContext,\n    createCircle,\n} from '@ripl/core';\n\n// Create a context to render the content to\n// Ripl uses a canvas context by default\nconst context = createContext('.mount-element');\n\n// Create an element\nconst circle = createCircle({\n    fillStyle: 'rgb(30, 105, 120)',\n    lineWidth: 4,\n    cx: context.width / 2,\n    cy: context.height / 2,\n    radius: context.width / 3\n});\n\n// Render the element to the context\ncircle.render(context);\n```\n\nRipl has a number of built-in elements such as arc, circle, rect, line, polyline, text, ellipse, and polygon. See [here](https://github.com/andrewcourtice/ripl/tree/5a60425a5f12fd34e8b4fe2864ebf9a66b03d212/packages/core/src/elements) for all available built-in elements.\n\n### Modify Element Properties\n\nTo modify an element simply change any of it's properties and re-render it.\n\n```typescript\nimport {\n    createContext,\n    createCircle,\n} from '@ripl/core';\n\nconst context = createContext('.mount-element');\nconst circle = createCircle({\n    fillStyle: 'rgb(30, 105, 120)',\n    lineWidth: 4,\n    cx: context.width / 2,\n    cy: context.height / 2,\n    radius: context.width / 3\n});\n\nfunction render() {\n    circle.render(context);\n}\n\nfunction update() {\n    circle.fillStyle = '#FF0000';\n    circle.cx = context.width / 3;\n    circle.cy = context.height / 3;\n\n    render();\n}\n```\n\n### Render to Different Contexts (eg. SVG)\n\nTo render the same element to SVG (or any other context) simply replace the import of the `createContext` method from `@ripl/core` to the appropriate package, in this case `@ripl/svg`. Here's the same example above rendered to SVG:\n\n```typescript\nimport {\n    createContext\n} from '@ripl/svg';\n\nimport {\n    createCircle,\n} from '@ripl/core';\n\nconst context = createContext('.mount-element');\nconst circle = createCircle({\n    fillStyle: 'rgb(30, 105, 120)',\n    lineWidth: 4,\n    cx: context.width / 2,\n    cy: context.height / 2,\n    radius: context.width / 3\n});\n\nfunction render() {\n    circle.render(context);\n}\n\nfunction update() {\n    circle.fillStyle = '#FF0000';\n    circle.cx = context.width / 3;\n    circle.cy = context.height / 3;\n\n    render();\n}\n```\n\n### Grouping and Inheritance\n\nRipl can also render multiple elements in groups with inherited properties (like CSS) and events (DOM event bubbling):\n\n```typescript\nimport {\n    createContext,\n    createCircle,\n    createRect,\n    createGroup\n} from '@ripl/core';\n\nconst context = createContext('.mount-element');\n\nconst circle = createCircle({\n    cx: context.width / 2,\n    cy: context.height / 2,\n    radius: context.width / 3\n});\n\nconst rect = createRect({\n    x: context.width / 2,\n    y: context.height / 2,\n    width: context.width / 5,\n    height: context.height / 5,\n});\n\nconst group = createGroup({\n    fillStyle: 'rgb(30, 105, 120)',\n    lineWidth: 4,\n    children: [\n        circle,\n        rect\n    ]\n});\n\ngroup.render(context);\n```\n\n### Querying Elements\n\nElements can be queried in groups using common DOM methods such as `getElementById`, `getElementsByType`, and `getElementsByClass`. Elements can also be queried using a subset of the CSS selector syntax with `query` and `queryAll`.\n\n```typescript\nimport {\n    createContext,\n    createCircle,\n    createRect,\n    createGroup\n} from '@ripl/core';\n\nconst context = createContext('.mount-element');\n\nconst circle = createCircle({\n    class: 'shape',\n    cx: context.width / 2,\n    cy: context.height / 2,\n    radius: context.width / 3\n});\n\nconst rect = createRect({\n    class: 'shape',\n    x: context.width / 2,\n    y: context.height / 2,\n    width: context.width / 5,\n    height: context.height / 5,\n});\n\nconst childGroup = createGroup({\n    id: 'child-group',\n    fillStyle: 'rgb(30, 105, 120)',\n    lineWidth: 4,\n    children: [\n        circle,\n        rect\n    ]\n});\n\nconst parentGroup = createGroup({\n    id: 'parent-group',\n    children: childGroup\n});\n\nparentGroup.render(context);\n\nfunction query() {\n    const qCircle = parentGroup.getElementsByType('circle');\n    const qRect = parentGroup.getElementsByType('rect');\n    const qChildren = parentGroup.queryAll('.shape');\n    const qChild = parentGroup.query('.child-group \u003e .shape');\n}\n```\n\nThe query selector currently only supports a limited subset of features compared to the DOM. Here are some examples of supported queries:\n```css\n// type\ncircle\n\n// id\n#element-id\n\n// class\n.element-class\n\n// element properties (attributes)\ncircle[radius=\"5\"]\n\n// descendents\n.group-class circle\n.group-class \u003e circle\n.parent-class .child-class rect\n.group-class rect + circle.circle-class\n.group-class \u003e rect[x=\"50\"]\n```\n\n### Scene Management\n\nRipl also provides complete scene management for rendering large group structures with events.\n\n```typescript\nimport {\n    createContext,\n    createCircle,\n    createRect,\n    createGroup,\n    createScene\n} from '@ripl/core';\n\nconst context = createContext('.mount-element');\n\nconst circle = createCircle({\n    class: 'shape',\n    cx: context.width / 2,\n    cy: context.height / 2,\n    radius: context.width / 3\n});\n\nconst rect = createRect({\n    class: 'shape',\n    x: context.width / 2,\n    y: context.height / 2,\n    width: context.width / 5,\n    height: context.height / 5,\n});\n\nconst childGroup = createGroup({\n    id: 'child-group',\n    fillStyle: 'rgb(30, 105, 120)',\n    lineWidth: 4,\n    children: [\n        circle,\n        rect\n    ]\n});\n\nconst parentGroup = createGroup({\n    id: 'parent-group',\n    children: childGroup\n});\n\nconst scene = createScene({\n    children: parentGroup\n})\n\nscene.render(context);\ncircle.on('click', event =\u003e console.log(event));\n```\n\n### Basic Animation and Interactivity\n\nInteractivity and animation can be added by using a renderer. The renderer provides an automatic render loop to re-render a scene at the ideal framerate.\n\n```typescript\nimport {\n    createContext,\n    createCircle,\n    createRect,\n    createGroup,\n    createScene,\n    easeOutCubic\n} from '@ripl/core';\n\nconst context = createContext('.mount-element');\n\nconst circle = createCircle({\n    fillStyle: 'rgb(30, 105, 120)',\n    cx: context.width / 2,\n    cy: context.height / 2,\n    radius: context.width / 3\n});\n\nconst rect = createRect({\n    fillStyle: 'rgb(30, 105, 120)',\n    x: context.width / 2,\n    y: context.height / 2,\n    width: context.width / 5,\n    height: context.height / 5,\n});\n\nconst scene = createScene({\n    children: [\n        circle,\n        rect\n    ]\n});\n\nconst renderer = createRenderer(scene, {\n    autoStart: true,\n    autoStop: true\n});\n\nasync function animate() {\n    // Render one\n    await renderer.transition(circle, {\n        duration: 1000,\n        ease: easeOutCubic,\n        state: {\n            fillStyle: '#FF0000',\n            cx: context.width / 4,\n            cy: context.height / 4,\n            radius: context.width / 4\n        }\n    });\n\n    // Render many with common properties\n    // Alternatively you could transition a whole group or scene\n    await renderer.transition([circle, rect], {\n        duration: 1000,\n        ease: easeOutCubic,\n        state: {\n            fillStyle: '#FF0000',\n        }\n    });\n}\n```\n\n### Advanced Animation\n\nRipl also supports CSS-like animation keyframes and custom interpolator functions\n\n```typescript\n// ...\n\nasync function animate() {\n    // Implicit keyframe offsets\n    await renderer.transition([circle, rect], {\n        duration: 1000,\n        ease: easeOutCubic,\n        state: {\n            fillStyle: [\n                '#FF0000', // implied 0.33 offset\n                '#00FF00', // implied 0.66 offset\n                '#0000FF', // implied end state - offset 1\n            ],\n        }\n    });\n\n    // Explicit keyframe offsets\n    await renderer.transition([circle, rect], {\n        duration: 1000,\n        ease: easeOutCubic,\n        state: {\n            fillStyle: [\n                {\n                    value: '#FF0000',\n                    offset: 0.25\n                },\n                {\n                    value: '#0000FF',\n                    offset: 0.8\n                }\n            ],\n        }\n    });\n\n    // Custom interpolator\n    await renderer.transition(circle, {\n        duration: 1000,\n        ease: easeOutCubic,\n        state: {\n            radius: t =\u003e t * context.width / 2 // where 0 \u003c= t \u003c= 1 (depending on the ease function)\n        }\n    });\n}\n```\n\n## Utilities\n\nRipl provides a number of utilities to enable rapid and straightforward developement of data visualizations.\n\n### Scales\n\nRipl currently provides 3 scale types (with more coming in the future) for mapping data between a specified domain and range. These scales are heavily influnced by [D3](https://d3js.org/d3-scale).\n\n#### Continuous (aka linear)\n\nMap continuous linear data from a given domain to a specified range.\n\n```typescript\nconst scale = scaleContinuous([0, 25], [-100, 100]);\nconst value = scale(10); // -20\n```\n\nLinear scales can also be clamped and inverted.\n\n```typescript\nconst scale = scaleContinuous([0, 25], [-100, 100], {\n    clamp: true\n});\n\n// With clamping\nscale(25) // 100\nscale(30) // 100 (clamped)\n\n// Invert\nscale.inverse(-20) // 10\n```\n\n#### Discrete\n\nMap discrete domain data to a continuous range.\n\n```typescript\nconst scale = scaleDiscrete(['a', 'b', 'c'], [0, 50]);\nconst value = scale('b'); // 25\n```\n\n## Performance\n\nRipl is designed to be as performant as possible, however, here are a few tips for getting the best performance out of it:\n\n1. **Using a scene + renderer to render many elements/groups** - When elements are placed into a scene and rendered using a renderer the elements are \"hoisted\" into a flat list essentially converting the render time from an O(n^c) operation to O(n). The performance cost is instead shifted to adding/removing elements from groups within the scenegraph.\n2. **Using persistent keys when creating paths in custom elements** - When creating a new path/text within a custom element it is highly encouraged to specify a persistent key for the path. The persistent key ensures that custom contexts such as svg can efficiently diff created objects with the DOM in each frame. This effectively reduces the amount of DOM elements that are required to be created/destroyed each frame. See an example of a persistent path key [here](https://github.com/andrewcourtice/ripl/blob/1a33451da6f1a3b5163b41189c00c0cce116accd/packages/core/src/core/shape.ts#L73).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrewcourtice%2Fripl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandrewcourtice%2Fripl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrewcourtice%2Fripl/lists"}