{"id":51624038,"url":"https://github.com/dannote/skia_ex","last_synced_at":"2026-07-12T22:32:18.407Z","repository":{"id":365080771,"uuid":"1255053933","full_name":"dannote/skia_ex","owner":"dannote","description":"Skia-backed Elixir drawing and rendering API with Rustler precompiled NIFs","archived":false,"fork":false,"pushed_at":"2026-07-07T15:12:30.000Z","size":741,"stargazers_count":11,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-07-12T22:32:16.471Z","etag":null,"topics":["2d-graphics","drawing","elixir","graphics","nif","renderer","rendering","rust","rustler","skia","sksl"],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/skia","language":"Elixir","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/dannote.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":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-31T10:47:25.000Z","updated_at":"2026-07-07T15:13:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dannote/skia_ex","commit_stats":null,"previous_names":["dannote/skia_ex"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/dannote/skia_ex","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fskia_ex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fskia_ex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fskia_ex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fskia_ex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dannote","download_url":"https://codeload.github.com/dannote/skia_ex/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fskia_ex/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35404819,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-12T02:00:06.386Z","response_time":87,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["2d-graphics","drawing","elixir","graphics","nif","renderer","rendering","rust","rustler","skia","sksl"],"created_at":"2026-07-12T22:32:17.906Z","updated_at":"2026-07-12T22:32:18.401Z","avatar_url":"https://github.com/dannote.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Skia\n\nBatched, Elixir-native drawing API backed by Rustler + `skia-safe`.\n\nThe public API builds immutable Elixir documents. Rendering crosses the native\nboundary once with a normalized command batch instead of calling a NIF for every\ncanvas operation.\n\n## Example\n\n```elixir\ndocument =\n  Skia.canvas(1200, 630)\n  |\u003e Skia.clear(\"#020617\")\n  |\u003e Skia.group([translate: {80, 80}], fn doc -\u003e\n    doc\n    |\u003e Skia.text(\"Launch Week\", x: 48, y: 90, size: 56, weight: 700, fill: :white)\n    |\u003e Skia.rect(x: 48, y: 140, width: 320, height: 96, radius: 24, fill: \"#3b82f6\")\n  end)\n\n{:ok, png} = Skia.to_png(document)\n{:ok, png} = Skia.render(document, format: :png)\n{:ok, %{width: width, height: height, stride: stride, data: rgba}} = Skia.render(document, Skia.RenderOptions.new(format: :raw))\n```\n\n## Paint sources and shaders\n\n```elixir\nfill =\n  Skia.Shader.two_point_conical_gradient({16, 16}, 0, {48, 48}, 32, [\n    Skia.Shader.stop(:red, 0),\n    Skia.Shader.stop(:blue, 1)\n  ], tile: :clamp, matrix: Skia.Matrix.rotate(0.2))\n\nsolid_shader = Skia.Shader.color(:green)\n```\n\nImage shaders support tile modes and rich sampling:\n\n```elixir\nSkia.Shader.image(image,\n  tile: {:repeat, :mirror},\n  sampling: Skia.SamplingOptions.cubic(:catmull_rom),\n  matrix: Skia.Matrix.scale(2, 2)\n)\n```\n\n## Filters and effects\n\nPaints can carry image filters, color filters, mask filters, blend modes, and path effects. You can pass options directly or build a reusable `%Skia.Paint{}`:\n\n```elixir\npaint = Skia.Paint.new(fill: :red, image_filter: Skia.ImageFilter.blur(2), blend_mode: :src_over)\nSkia.rect(doc, x: 0, y: 0, width: 100, height: 100, paint: paint)\n\ncolor_filter =\n  Skia.ColorFilter.blend(:blue, :src_in)\n  |\u003e Skia.ColorFilter.compose(Skia.ColorFilter.matrix([\n    1, 0, 0, 0, 0,\n    0, 1, 0, 0, 0,\n    0, 0, 1, 0, 0,\n    0, 0, 0, 1, 0\n  ]))\n\npath_effect =\n  Skia.PathEffect.trim(0.05, 0.95)\n  |\u003e Skia.PathEffect.compose(Skia.PathEffect.dash([8, 4]))\n  |\u003e Skia.PathEffect.sum(Skia.PathEffect.discrete(6, 1.5, seed: 42))\n\nstamp = Skia.Path.new() |\u003e Skia.Path.move_to(0, 0) |\u003e Skia.Path.line_to(1, 1)\nadvanced_effect = Skia.PathEffect.path_1d(stamp, 8, style: :rotate)\n\nSkia.path(doc, path,\n  stroke: :red,\n  stroke_width: 2,\n  blend_mode: :src_over,\n  color_filter: color_filter,\n  image_filter: Skia.ImageFilter.blur(2),\n  mask_filter: Skia.MaskFilter.blur(1.5, style: :normal),\n  path_effect: path_effect\n)\n```\n\nLayer and image-filter graphs are composable:\n\n```elixir\nfilter =\n  Skia.ImageFilter.blur(2)\n  |\u003e Skia.ImageFilter.compose(Skia.ImageFilter.offset(4, 2))\n  |\u003e Skia.ImageFilter.compose(Skia.ImageFilter.matrix_transform(Skia.Matrix.scale(1.2, 1.2)))\n\nadvanced = Skia.ImageFilter.merge([\n  Skia.ImageFilter.magnifier({0, 0, 100, 100}, 1.5, 4),\n  Skia.ImageFilter.tile({0, 0, 20, 20}, {0, 0, 100, 100}),\n  Skia.ImageFilter.matrix_convolution({1, 1}, [1.0])\n])\n\nSkia.layer(doc, [image_filter: filter], fn layer -\u003e\n  Skia.rect(layer, x: 0, y: 0, width: 80, height: 80, fill: :red)\nend)\n```\n\n## Paths\n\nPaths are immutable Elixir structs and support absolute, relative, conic, cubic,\nSVG import, and SVG export:\n\n```elixir\npath =\n  Skia.Path.new()\n  |\u003e Skia.Path.move_to(0, 0)\n  |\u003e Skia.Path.r_line_to(40, 0)\n  |\u003e Skia.Path.conic_to(60, 20, 40, 40, 0.5)\n  |\u003e Skia.Path.arc_to({0, 0, 80, 80}, 0, 180, force_move_to: false)\n  |\u003e Skia.Path.rrect({10, 10, 40, 24}, 6)\n  |\u003e Skia.Path.close()\n\nsvg_path = Skia.Path.from_svg(\"M0 0L100 0L100 100Z\")\n{:ok, svg} = Skia.Path.to_svg(svg_path)\n```\n\n## Pictures\n\nRecord reusable drawing subtrees as Skia pictures and replay them later:\n\n```elixir\n{:ok, picture} =\n  Skia.canvas(64, 64)\n  |\u003e Skia.rect(x: 0, y: 0, width: 64, height: 64, fill: :red)\n  |\u003e Skia.Picture.record()\n\n{:ok, info} = Skia.Picture.info(picture)\n{:ok, bytes} = Skia.Picture.encode(picture)\n{:ok, picture} = Skia.Picture.decode(bytes, width: 64, height: 64)\n{:ok, image} = Skia.Image.from_picture(picture)\n\nSkia.canvas(256, 64)\n|\u003e Skia.picture(picture, x: 96, y: 0, opacity: 0.75)\n\nSkia.rect(doc, x: 0, y: 0, width: 128, height: 128, fill: Skia.Shader.picture(picture))\n```\n\n## Fonts and typefaces\n\n`Skia.Typeface` represents the reusable font face; `Skia.Font` adds size for drawing and measurement.\n\n```elixir\n{:ok, families} = Skia.Typeface.families()\n{:ok, typeface} = Skia.Typeface.match_family(\"Inter\", weight: 700, slant: :upright)\nfont = Skia.Font.new(typeface, size: 24)\n{:ok, info} = Skia.Typeface.info(typeface)\n{:ok, metrics} = Skia.Font.metrics(font)\n{:ok, glyphs} = Skia.Font.glyph_ids(font, \"Hello\")\n{:ok, measurement} = Skia.measure_text(\"Hello\", font: font)\n```\n\nLoaded images, fonts, and pictures keep decoded Skia handles in their native\nresources for repeated drawing, while pictures still retain serialized bytes for\n`Skia.Picture.encode/1`.\n\n## Text\n\nUse direct options for convenience or reusable style structs for paragraph text:\n\n```elixir\nstyle = Skia.TextStyle.new(size: 16, fill: :black, font_family: \"Arial\", line_height: 20)\nparagraph = Skia.ParagraphStyle.new(width: 320, align: :center, direction: :ltr)\n\nspans = [\n  Skia.TextSpan.new(\"Hello \", fill: :red, size: 18),\n  Skia.TextSpan.new(\"Skia\", fill: :blue, size: 24)\n]\n\nSkia.text(doc, \"Hello\", x: 0, y: 0, style: style, paragraph_style: paragraph)\nSkia.text(doc, \"\", x: 0, y: 32, paragraph_style: paragraph, spans: spans)\n\n{:ok, blob} = Skia.TextBlob.new(\"Cached\", font: font)\n{:ok, bounds} = Skia.TextBlob.bounds(blob)\nSkia.text_blob(doc, blob, x: 0, y: 64, fill: :black)\n```\n\n## Runtime effects / SkSL\n\nCompile reusable SkSL shader effects and pass uniforms from Elixir:\n\n```elixir\n{:ok, effect} =\n  Skia.RuntimeEffect.compile(\"\"\"\n  uniform float time;\n  uniform vec2 resolution;\n\n  half4 main(vec2 p) {\n    vec2 uv = p / resolution;\n    return half4(uv.x, 0.5 + 0.5 * sin(time + uv.x * 20.0), uv.y, 1.0);\n  }\n  \"\"\")\n\nshader = Skia.RuntimeEffect.shader(effect, uniforms: %{time: 1.2, resolution: {800, 600}})\nSkia.rect(doc, x: 0, y: 0, width: 800, height: 600, fill: shader)\n```\n\nRuntime effects also support int uniforms, child shaders, and one-off compile helpers:\n\n```elixir\nshader =\n  Skia.Shader.sksl!(\"\"\"\n  uniform shader child;\n  uniform int enabled;\n\n  half4 main(vec2 p) {\n    return enabled == 1 ? child.eval(p) : half4(0, 0, 0, 1);\n  }\n  \"\"\",\n  uniforms: %{enabled: Skia.RuntimeEffect.int(1)},\n  children: %{child: Skia.Shader.color(:red)})\n```\n\n## Vertices\n\nDraw triangle meshes with per-vertex colors:\n\n```elixir\nvertices = Skia.Vertices.new([{0, 0}, {100, 0}, {50, 100}], colors: [:red, :green, :blue])\nSkia.vertices(doc, vertices)\n```\n\n## Compact batches and benchmarking\n\n`Skia.to_batch/1` returns the normal map/struct batch consumed by the native renderer.\nEncode normal batches directly with `:erlang.term_to_binary(Skia.to_batch(document))`.\n`Skia.Compact.encode/1` uses stable operation ids and compact color/path values, and\n`Skia.Compact.encode_binary/1` writes compressed ETF. Compact batches can also render\nthrough native compact decode:\n\n```elixir\n{:ok, raw} = Skia.Compact.to_raw(document)\n{:ok, png} = Skia.Compact.render(document, format: :png)\n\n{:ok, stats} = Skia.Benchmark.compare(document, iterations: 20)\nstats.normal_batch_bytes\nstats.compact_batch_bytes\nstats.compact_render_us\nstats.picture_replay_us\n```\n\nNative operations return tagged errors such as `:invalid_image`, `:invalid_picture`,\n`:invalid_path`, and `:invalid_command` instead of leaking raw NIF badarg failures\nthrough public helpers.\n\n## Development\n\n```sh\nmix deps.get\nmix ci\n\n# Force local Rustler build instead of downloading a precompiled NIF.\nSKIA_EX_BUILD=1 mix compile\n```\n\nSee `docs/commands.md` for the generated command reference.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdannote%2Fskia_ex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdannote%2Fskia_ex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdannote%2Fskia_ex/lists"}