Files
Graphite/.jjconflict-base-0/website/content/blog/2024-01-01-looking-back-on-2023-and-what's-next.md
Keavon Chambers a708a54492 Make the data model use Item and List types universally, with nodes authored as rank-polymorphic kernels (#4335)
* Add rank polymorphism node audit classifying all 271 nodes

* Implement StaticType for Item<T>

* Generate Item and mapped List wire variants for nodes declaring an Item<T> primary input

* Migrate nine nodes to Item element-wise kernels, dissolving the blending trait boilerplate

* Document the Item kernel implementation and staging plan

* Route Item<Vector> through TaggedValue::TypeDefault

* Add executor integration tests covering the Item and List wire variants

* Collapse element-wise Item/List wire pairs to the List form for conversion insertion

* Migrate sixteen vector modifier nodes to Item element-wise kernels

* Migrate Sample Image, Extend Image to Bounds, and Dehaze to Item element-wise kernels

* Fix bevel_with_transform test to actually exercise the transform attribute

* Implement From<T> for Item<T>

* Register PromoteNode rank adapters wrapping bare values into Item wires

* Insert PromoteNode adapters for Item/List wire pair fields in the preprocessor

* Define a real promote node backing the PromoteNode registry identifiers

* Zip ranked Item connectors by frame slot in the mapped element-wise variant

* Register ItemToListNode singleton raise adapters

* Resolve Item wires against List connectors by inserting promotion adapters at construction

* Rank the Offset Points distance connector and prove mixed-rank resolution end-to-end

* Implement Clampable for Item and List wires with per-variant clamp bounds

* Rank the Round Corners radius connector, exercising hard bounds on a ranked wire

* Implement ApplyTransform for Item

* Add Item wire implementations to the Transform node, keeping rank-0 chains rank 0

* Detect element-wise nodes by lazy primary connectors declaring Output = Item

* Convert Transform to an Item kernel with ranked parameters, delivering the broadcast milestone

* Rename Apply Transform to Bake Transform, baking item transforms on Vector, DAffine2, and DVec2

* Promote bare wires onto Item connectors at resolution via WrapItemNode adapters

* Rank the numeric, vector, and boolean parameters across the migrated element-wise nodes

* Rank the enum, integer, and seed parameters, registering their rank adapters via a consolidated macro

* Amend the audit with the DashPattern value type resolution

* Migrate the string family to Item element-wise kernels

* Unwrap Item wires into bare legacy connectors at resolution via UnwrapItemNode adapters

* Shadow owned node parameters in bodies instead of mut in signatures

* Migrate the math family and string measure nodes to Item element-wise kernels

* Convert the comparison and clamp nodes to Item kernels, dropping unreachable &str rows

* Flat-map expander kernels returning List under the mapped variant's frame

* Migrate the expander nodes to Item kernels flat-mapping under the frame

* Remove the unused peel_list helper

* Rank the raster adjustment and blending kernels, recontextualizing shader nodes onto an Item stand-in

Migrate the 16 adjustment nodes, Mix, Color Overlay, and Gradient Map from whole-List kernels to rank-0 Item kernels, letting the macro derive the List-mapped (zip) variants. Move the Adjust and Blend per-element seams off List onto the element types (add the Raster<CPU> impls, drop the now-dead List impls).

Shader nodes keep their bodies verbatim: PerPixelAdjust re-emits the identical kernel against a transparent no_std Item stand-in, so every Item<T> connector and .element() call resolves to a zero-cost identity on the GPU while the uniform buffer stays bare repr(C). The macro peels Item off ranked uniform params, wraps the fetched texel and uniforms at the entry point, and unwraps the result. This drops the shader_node/Item incompatibility guard. Register rank adapters for the adjustment enums.

* Update the rank polymorphism roadmap for the landed shader-node and adjustments chunk

* Rename the GPU Item stand-in to ShaderItem, aliased as Item at its shader-node import sites

* Flip the vector shape generators to emit rank-0 Item<Vector>

The shape generators (Rectangle, Circle, Ellipse, Arc, Spiral, Polygon, Star, Arrow, Line, Grid, QR Code) each produced exactly one shape wrapped in a singleton List<Vector>. Emit Item<Vector> directly so they connect to the rank-0 content connector of the migrated Transform node. Downstream List consumers receive the value through the existing Item to List promotion.

Relax the element-wise validation so a `()` (generator) primary may return Item<T> without being element-wise. Adapt the Repeat on Points test, which still takes a List content connector, by raising the generator's Item output through a singleton wrapper node.

* Parse ranked Item<T> parameter defaults against the bare element type

A ranked `Item<T>` parameter's default value is a bare, unranked `T` (promoted to the wire at resolution), but the preprocessor was handed the wrapped `Item<T>` type and could not parse the literal, flooding the console with warnings and dropping the defaults. Key the field's default_type metadata off the peeled element type for concrete ranked parameters, leaving generic `Item<T>` primaries and skip_impl nodes untouched.

* Parse an element-wise primary's scalar default against the bare element type

An element-wise node's primary reports its default_type as the List wire form so an unconnected primary defaults to an empty list. But when the primary carries a scalar `#[default]` (such as Root's radicand), that literal must parse as a bare element, not a List. Key the primary's default_type off the bare element type when it has a Default value source, keeping the List form otherwise.

* Add the DashPattern value type for stroke dash sequences

Introduce a rank-0 DashPattern value type (a Vec<f64> of alternating dash and gap lengths) so a stroke's dash pattern is a single frameable value rather than a rank-1 List<f64>. Register it as an auto-generated TaggedValue variant, parse its default from a comma or space separated string, and register its rank adapters. Not yet wired into the Stroke node.

* Rank the Fill and Stroke nodes element-wise and give Stroke a DashPattern connector

Migrate Fill and Stroke to element-wise Item<V> primaries (over Vector and Graphic element types) via a new element-level VectorItemMut trait, so styling one shape yields one shape and rank is preserved instead of promoting the input to a singleton List and emitting a List. The macro derives the List-mapped variant for genuine collections.

Wire the Stroke dash sequence to the new rank-0 DashPattern value type, collapsing the old content x paint x dash cartesian and dropping the IntoF64Vec trait. Update the stroke properties dash widget, the drawing tool, and graph-operation plumbing to read and write DashPattern, and migrate legacy F64Array, F64, and String dash inputs on document open.

Assign Colors stays a whole-collection node: each element's gradient position depends on its index among all siblings, which the element frame does not expose, so it keeps its List primary and the VectorListIterMut trait.

* Register rank adapters for the ranked Stroke enum parameters

The element-wise Stroke node ranks its align, cap, and paint order parameters as Item<StrokeAlign>, Item<StrokeCap>, and Item<PaintOrder>, but those enums lacked promotion adapters, so a bare default enum value could not be promoted to its Item wire and no Stroke variant resolved ("No construct found for node"). Register their rank adapters alongside StrokeJoin.

* Display Item wires in the Data panel without a List's ID column

Add a TableItemLayout impl for Item<T> and recognize Item wire types when introspecting graph data. An Item holds a single element, so it renders as a one-row table of the element plus its attributes with no leading index column, and it labels as its element type T rather than a List's T[]. Add ItemAttributeValues::get_any for the attribute widget dispatch.

* Register MonitorNode for Item wire types so the Data panel introspects them directly

Graph introspection wraps the inspected output in a generic MonitorNode typed to the wire. Without Item<T> monitor registrations, an Item<Vector> output could only be monitored after an Item to List promotion, so the Data panel captured and displayed a List<Vector> despite the connector being Item<Vector>. Register monitors for the Item types the element-wise nodes emit, and add the matching Data panel downcast entries.

* Color and double Item/List wires and cleave layer-stack connectors in the node graph

* Route wire color and rank through hidden nodes and refresh them on type changes

* Rework the DashPattern connector conversions with element-wise promotion and an explicit reducer node

* Rank the remaining value, context, aggregation, and transform nodes onto Item<T> wires

* Back DashPattern with a List<f64> so the Data panel can introspect its lengths

* Carry a single Item<T> through varargs so the Read context nodes emit Item<T> not List<T>

* Relax rank validation for aggregation shapes, add element adapters, and match variants by fewest promotions

* Rank the remaining bare and unnecessarily-List connectors across the node catalog

* Add Graphic::None and the FillChoice paint value, making colors and gradients plain values

* Rename GradientStops to Gradient and the legacy Gradient/Fill structs to LegacyGradient/LegacyFill

* Restore generator frame-from-params ranking to the roadmap as a planned stage

* Rename the ranked-field adapter identifier from PromoteNode to FieldAdapterNode to reflect its full contract

* Unload only the wires whose displayed style changed when types update

* Peel wire rank in the editor's semantic type checks so rank-0 layers are recognized

* Restore the whole-List Transform variant so rank-1 content wires resolve again

* Register the Item wire forms for the Memoize and Context Modification infrastructure nodes

* Give every ranked connector a field adapter and add numeric cast variants for legacy wires

* Key a ranked param's type default off its Item wire form when no literal default exists

* Inherit the layer's content value when splicing a node into an empty chain

* Migrate stale List-form TypeDefault inputs to the definition's current default

* Generate the mapped wire variant only when the element-wise node has a frame source

* Let a bare wire feed a List connector via a wrap-raise adapter, costed as two rank steps

* Add a zip companion to the whole-List Transform so ranked List parameters pair per slot

* Add the Sum, Average, Minimum, Maximum, Any, and All list reducers

* Convert the measure family to element-wise Item kernels per the audit classification

* Prefer the bare element value over the Item type default so ranked params keep their widgets

* Rename GradientStopsUI to GradientUI

* Split Fill's optional transform into a _has_transform bool and a ranked _transform matrix

* Rename the migration-only OptionalDAffine2 TaggedValue to LegacyOptionalDAffine2

* Flow byte buffers as Item<Resource> instead of List<u8> across the byte nodes

* Macro-generate the list-content wire variant, retiring the hand-written Transform-zip, Area, and Centroid companions

* Let ()-primary generators take ranked params and frame over them via the mapped variant, ranking Circle's radius

* Rank the vector shape generators' params to Item, adding a rank-aware input grab to the introspection harness

* Rank the value, color, and text generator params to Item

* Rank the raster, web-request, and context-reader generator params to Item

* Fix the repeat and brush test wirings left behind by the param-ranking sweeps

* Delete the vestigial Some, Unwrap Option, and Size Of debug nodes

* Delete the Attach Attribute node, folding its role into Write Attribute

* Add the Filter and Sort list companion nodes

* Guard the removed-definition migration swap target with a test

* Add the Box Corners value type in place of the rectangle corner radius list

* Split Text to Vector's per-glyph mode into a Text to Vector Glyphs node

* Rank the Combine Channels node's channel connectors to Item

* Make Map Points an element-wise node

* Delete the deprecated Upload Texture node

* Update the implementation roadmap to reflect the landed stages

* Let monitor introspection read rank-0 wires, locking in the layer coercion promotion path

* Prefer the rank-0 default when disconnecting a rank-capable input

* Make Path Modify an element-wise node

* Wrap node paths in a NodeIdPath newtype so they flow as a single Item

* Give Item<Raster<CPU>> a default so an unconnected Brush background resolves

* Stop the Brush node from setting layer attributes its paint operation doesn't produce

* Present-gate Flatten Path's adopted layer path like its fill and stroke

* Gate carried layer attributes on static column presence, not runtime values

* Give the remaining graphic Item<T> types a default so unconnected primaries resolve

* Dispatch a ranked param's Properties widget from its rank-0 element type

* Make Extract Transform an element-wise node, restoring the Origins to Polyline body

* Rename Flatten Path to Combine Paths

* Stamp Legacy Layer Extend's adopted layer path as a readable NodeIdPath

* Drop the dead List<u8> and List<NodeId> wire rows

* Rank Flatten Graphic's Fully Flatten toggle to Item

* Update the implementation roadmap with the endgame scope

* Make Combine Paths a reducer that collapses the whole frame into one path

* Stop type-converter nodes from carrying the source's unrelated attributes

* Format the Origins to Polyline regression test

* Wrap the Brush node's trace in a BrushTrace newtype so it flows as one value

* Make Switch a framed element-wise select, bundling whole collections

* Widen and align element-type coverage across the list and graphic nodes

* Register the compiler's cache chain pair for every ranked enum and newtype wire

* Fix wire colors for Passthrough outputs, bundled lists, and bools, and widen list wires

* Represent List wire types structurally with Type::List, replacing name-parsed rank promotion

* Treat scope and data fields as environment, rank scope wires as Item, and feed the render boundary through a context vararg

* Delete the vestigial Clone debug node

* Reinstate Upload Texture as an element-wise node and fix the GPU variants' scope executor and rank adapters

* Rename Combine Paths back to Flatten Path, deferring that rename to its own PR

* Deduplicate the promotion adapter registrations into the field adapter macro

* Rank Write Attribute's value connector to Item<AttributeValueDyn>, retiring the UnwrapItem bridge

* Vertical wire styling

* Store the editor layer path attribute as a bare NodeIdPath, not an Item<NodeIdPath>

* Rank Context Modification's features connector to Item<ContextFeatures>, dropping the dead memoize row

* Rank Path Modify's modification parameter to Item<Box<VectorModification>>

* Rename the field adapter node family to input adapter

* Drop the dead bare scalar rows from Context Modification's implementations list

* Move the dynamic executor's test module into its own file

* Drop the registry's unreachable bare rows for Memoize, the cache chain, and ConvertNode

* Materialize stored TaggedValues as ranked Item wires at the source

* Remove the bare-wire promotion and adapter machinery made dead by ranked value materialization

* Plant the input adapter for List-only inputs, composing position conversion from standard rows

* Consolidate Into/Convert conversions into the input adapter umbrella and rename the rank adapter identifiers

* Fix grouped layers gaining a phantom None stack element from the FillChoice default hijacking every List<Graphic> disconnect

* Enforce ranked node inputs in the macro, rejecting bare wire declarations

* Remove the unit Context => () machinery rows, leaving () purely as the no-primary sentinel

* Add a --signatures rank-audit mode to node-docs for the ranked-wire migration

* Remove the node-docs --signatures rank-audit mode now that ranked wires are enforced

* Migrate legacy no-color values on the Black & White, Color Overlay, and Empty Image color inputs

* Rewrite the element-wise accessor wire type at the primary input, not raw index 0

* Register the cache chain for Resource wires, replacing the lone hand-written Monitor row

* Gate the remaining Raster<GPU> registry rows behind the gpu feature

* Let List<DVec2> wires erase to ListDyn for the attribute reader and element counter

* Rename Extract Element to Item at Index, Count Elements to List Length, and Omit Element to Remove at Index

* Store paint picks as plain color/gradient values, removing the FillChoice value type

* Code review restructuring

* Sort by the consumed sort_key attribute or natural element order, adding the Sort Key node

* Remove the new list-combinator and reducer nodes to defer them to a follow-up PR

* Parse Fill and Stroke color defaults through the paint wire's Graphic element

* Emit ranked implementation-row default types structurally so their element TypeIds survive to default-literal parsing

* Exempt the deliberate no-paint choice from the stale List-form TypeDefault migration

* Migrate the legacy 4-input Fill directly to the split has-transform shape

* Upgrade the demo artwork

* Fix the valid AI review findings: Item eq/hash contract, table-era no-paint migration, quantize List rows, and other smaller issues

* Remove the rank polymorphism working documents

* Hash Item attribute values directly instead of debug-formatting them, speeding up cached evaluation

* Replace the data panel's dead bare-wire downcast arms with full coverage of the ranked monitor row types

* Derive PartialEq for Item now that attributes participate in equality

* Extend the data panel's attribute dispatchers with the newly supported scalar and choice enum types

* Add List monitor rows for the framed numeric conversion outputs so inspecting them resolves, with matching data panel arms
2026-09-08 16:03:01 +00:00

27 KiB

+++ title = "Looking back on 2023 and what's next" date = 2024-01-01

[extra] banner = "https://static.graphite.art/content/blog/2024-01-01-looking-back-on-2023-and-what's-next.avif" banner_png = "https://static.graphite.art/content/blog/2024-01-01-looking-back-on-2023-and-what's-next.png" author = "Keavon Chambers" summary = "Looking back on 2023, we reflect on our significant achievements and milestones. As we move forward, we're excited to share what's next, promising a year filled with innovation and progress." reddit = "https://www.reddit.com/r/graphite/comments/18xmoti/blog_post_looking_back_on_2023_and_whats_next/" twitter = "https://twitter.com/GraphiteEditor/status/1742576805532577937"

js = ["/js/component/youtube-embed.js"] css = ["/component/youtube-embed.css"] +++

The new year is here, and with so many accomplishments to share from the past twelve months, let's revisit the highlights of 2023 for the Graphite project. Now that winter has entered, let's swing back to the spring, summarize the summer, and follow this fall's noteworthy developments that brought another year of fruitful progress to Graphite's mission of re-envisioning artists' 2D creative workflows with the best free software we can build for the open source community. This past year as a team, we all got closer— to one another from continents apart; to visiting and connecting with our industry peers; and to reaching exciting new development milestones.

I am grateful to everyone who has placed their faith in my vision for Graphite since I laid forth the design and wrote its first line of code nearly three years ago. Meeting some of the amazing people this summer who helped to make it possible, and inspired the project in the first place, was a pleasure and an honor. From California to Europe and back again, my combined family vacation and Graphite outreach tour was an opportunity to make connections with those helping us reach our goals. This blog post is both a project update and a public thank-you to those who generously lent their time and attention to our small-but-growing project. And for readers eager for an update on the software itself, stick around (or skip ahead) for a development progress report and a look at what's coming down the pipeline in the new year.

Happy Holidays from the Graphite team!
These procedural light strands are powered by the newly completed node graph features.
Click here to explore this demo — drag the wire layer's points with the Path tool.

The Alpha 2 release series

February marked the start of our second year developing Graphite under the alpha release banner. We took the opportunity to declare the start of a new release series, Alpha 2, for the year's focus on integrating the node graph. That goal was a success, and we anticipate the next release series, Alpha 3, will begin next month in February with a focus on procedural art workflows in line with our roadmap.

GDC and meetups with the Rust graphics community

In March, I attended the Game Developers Conference (GDC) in nearby San Francisco to network with professionals in the creative industry. I was accompanied by Oliver Davies, a Graphite founding collaborator, fellow 3D artist and graphics engineer, and a life-long friend of mine. We connected with Francesco Siddi and Dalai Felinto from the Blender Foundation and introduced them to the project. The conference was also an opportunity to meet face-to-face with subject-matter experts with whom I'd earlier conversed online. Because we write Graphite in the Rust programming language, we attended an impromptu meetup among game developers using Rust in the nearby park that stretched for several insightful hours of pertinent conversation topics, thusly concluding the last day of the event.

Later, in May, I went to another Rust developers meetup here in the Bay Area together with Leonard Pauli, a Graphite community member and code contributor who was in town on a visit all the way from Sweden. The event was headlined with a presentation by Raph Levien about Xilem, an under-development GUI toolkit that Graphite may adopt someday for its promise of powering native, speedy desktop user interfaces. Raph Levien is a researcher and expert in the fields of 2D vector graphics, GPU-accelerated rendering, and the mathematics of splines and curves— topics considerably overlapping with Graphite's own technical disciplines. The meetup was a nice face-to-face introduction before I'd end up seeing Raph and Leonard each again very soon.

Raph Levien speaking about Xilem

Raph gives his talk about Xilem and GUIs in Rust

The next week I accepted Raph's invitation to visit him at his employer, Google, in Mountain View where we spent several hours talking shop. His other flagship open source library, Vello, is the high-performance 2D vector graphics renderer we plan to use as a crucial part of Graphite's render pipeline. Our discussions dove into the history and goals of Graphite, our shared research challenges, and covered some fascinating details surrounding Vello, computational geometry, and GPU rendering. As we round out 2023, Graphite's roadmap is finally nearing the stage of integrating Vello in the coming weeks and I look forward to growing our collaboration with Raph and his research group.

Embark Studios visit in Stockholm, Sweden

Leonard and Keavon in front of the Embark office sign

Leonard (left) and Keavon (right) at Embark Studios

In June, as part of a vacation with my family to Europe, I caught up again with Leonard and he led us on a tour of his beautiful city of Stockholm. I also reached out to Johan Andersson, CTO of Embark Studios and an ambassador for the open source Rust developer community. Embark very generously contributes open source libraries for the Rust computer graphics ecosystem that are vital to Graphite, including rust-gpu which powers our node graph engine's compilation to GPU compute shaders. Johan showed Lenoard and me around Embark and we all chatted about what each of us are pursuing with Rust in the creative software industry. (By the way, Embark just released their first game, The Finals— check it out!)

Blender visit in Amsterdam, The Netherlands

Graphite and Blender team members in front of the Blender building

Left to right: Dalai, Dennis, Keavon, Ton, Francesco

The next month in July, together with Graphite's lead engineer Dennis Kobert, we spent the afternoon visiting Blender's headquarters in Amsterdam. Blender has been, since the beginning, my inspiration and motivation for taking on the tremendously ambitious goal of building Graphite. As fellow open source software projects building digital content creation tools, this was a wonderful chance to see where the magic happens and meet the people behind the curtain.

At the invitation of Francesco Siddi, COO of the Blender Foundation whom I'd met earlier at GDC, Dennis and I presented a lunchtime talk for the staff to introduce the Graphite project. Ton Roosendaal, Blender's founder, kindly cooked up some scrumptious, lovingly-made meals of fried eggs for us and his team. We spent a couple hours mingling— answering and asking questions and chatting about design and technical topics ranging from our Rust node graph language infrastructure to Blender's experience with color science standards.

Keavon presents in front of a projector screen at Blender's offices

Keavon presents to the Blender staff

Ton also very generously gave us his time and full attention with an office hour for Dennis and me to ask his advice and learn from the three decades of experience that led to Blender's decisive success. (On January 2, Blender turns 30! They're aiming for a goal of 10,000 supporters to donate a birthday gift. I encourage you to join me in doing so.)

Graphite developer retreat in Karlsruhe, Germany

Keavon and Dennis in front of Karlsruhe Palace

Keavon (left) and Dennis (right) at Karlsruhe Palace

The week before the Blender visit, I arrived in Karlsruhe, Germany for a two-week stay with Dennis for our first-ever team retreat. After spending hundreds of hours collaborating online, it was great to finally meet in-person. From diving deep into Graphite design discussions, to exploring the city and his college campus and joining his friends in social activities (and thus meeting another Graphite contributor, Isaac Körner, recruited by Dennis), it was a very welcoming and productive exchange.

SIGGRAPH in Los Angeles, California

Keavon and Oliver standing in front of the SIGGRAPH conference sign

Keavon (left) and Oliver (right) at SIGGRAPH

After returning back home to the States, the following month in August I road tripped down south to Los Angeles for another conference visit with Oliver Davies. SIGGRAPH is the industry's largest computer graphics conference which presented numerous opportunities to network with others in the field. Blender also exhibits each year and I got to meet up again with the crew from Amsterdam and meet some new faces who were absent in our earlier visit. I look forward to returning to LA this coming April for the first full Blender Conference held in the US and connecting with more like-minded open source creative aficionados.

Incorporating Graphite Labs, LLC

The next big news of August was my formation of Graphite Labs, LLC as a legal entity, allowing us to open a bank account and sign contracts on behalf of the Graphite project. This is an important step in the professional growth of the project. A tax-exempt nonprotfit foundation may happen in the future, but an LLC is a more accessible starting point. This step has opened up opportunities to form partnerships with industry, collect sponsorships and donations, and hire full-time engineers a few years down the road once we have the income to financially support others developing Graphite. A major goal in 2024 is growing the sustainability and financial independence of the organization and allowing myself a modest income stream to offset costs while continuing my full-time Graphite work.

Website, user manual, and tutorials

I allocated my time at several points throughout the year into growing and evolving this website with a refreshed and more visually-appealing home page, dedicated pages for information about the project and its features, an area providing resources and help for volunteers and code contributors, and just this month— a user manual complete with an introductory tutorial series. The first video went up yesterday:

{{ youtube_embed(id="7gjUhl_3X10", title="Vector Art Quickstart - Graphite, the Open Source 2D Graphics Suite") }}

The user manual and tutorial series will continue expanding throughout the coming weeks. Additional website features including user accounts, forums, and other community features are being planned.

2023 development progress report

Graph visualizing the number of code commits each week of 2023

Weekly code commit rate in 2023

Starting out the year twelve months ago, Graphite's core vector editing tools were in good shape and the node graph engine had just reached its experimental beginnings as a way of applying color filters to bitmap images. Further developing the graph engine (called Graphene) and integrating it throughout every part of the Graphite editor thus became the focus for 2023.

While much of the team's time was spent on refactors to swap short-term placeholder code with Graphene-powered replacements, this one-step-back, two-steps-forward approach has ultimately led to exciting new capabilities for users to design procedurally-generated vector art. I'm aware of no other vector graphics editor with a node-based procedural editing workflow, so this makes me especially thrilled to release the first app of its kind with that unique and useful capability.

But 2023 wasn't only put towards refactoring code. New features were added all throughout the year and here are a few favorites.

  • Graphene's node infrastructure has seen steady developments which significantly upgraded the power of the node graph, its performance, and its frontend usability.
  • There are lots of new nodes that do neat things! From complex color adjustments like Vibrance to procedural building blocks like Copy to Points and noise pattern generators, there are plenty to try out (and so many more coming in 2024).
  • The (still rudimentary) Brush tool was added for drawing simple raster-based sketches.
  • Drawing custom vector shapes with the Pen and Path tools saw usability improvements with point selection, nudging/transformation, and entering exact numerical positions.
  • Number input boxes in the UI can now be dragged to update their values and have math expressions evaluated automatically for convenience. Double a value just by typing *2 at the end, or take the square root by wrapping it within sqrt( and ).
  • A button to quickly open pre-made sample art documents was included at the suggestion of the fine folks at Blender. This helps new users see Graphite in action instead of just opening up an overwhelmingly barren blank canvas.

Integrating the node graph

With so much to change in the goal of rewriting nearly every system with its Graphene counterpart, we had to take an incremental path so other feature development could continue without a broken editor. As succinctly as possible, this is the story of how we pulled it off— although this section gets rather technical so feel free to skip past if that's not your cup of tea.

The previous, intentionally-temporary layer system supported folders, vector shapes, text, bitmap images, and dynamic image generation.

Block diagram of a Graphite document before the node graph

Block diagram before the node graph

To begin the incremental integration of nodes, we first added another type of layer, housing an instance of a Graphene node graph, which would supersede the other types. Then began the long process of porting all the other "legacy" layer types—and the viewport tools that operated on them—to become nodes. So the text layer type became a Text node managed by the Text tool, for example. This first phase was completed by April.

Block diagram of a Graphite document after the first phase of migration

Block diagram after the first phase

Just folders and graph-driven legacy layers remained, but for the second phase, these (as well as artboards which were a separate temporary system) had to be combined into a single graph. Even with graph-based legacy layers, nodes couldn't yet interact between layer graphs to create interesting procedural designs. The Graphite vision calls for a single unified graph per document where all content lives, organized by layers, folders, and artboards collectively living within that graph. So phase two began with adding another node graph instance attached to the document itself instead of any particular layer. Then the old artboard system was replaced by artboard nodes in that graph, providing white backgrounds for the pages of artwork drawn atop by layers. Next, we built a new form of layer that would live in the node graph itself to provide organizational structure, acting as both a container for artwork nodes and a folder for other layers. By August, users could edit the document graph by hand but the viewport tools, folder hierarchy, rendering pipeline, and numerous other systems all still used the legacy layers.

Block diagram of a Graphite document after the second phase of migration

Block diagram after the second phase

For the third phase, we had to yet again port each of the viewport tools so they would operate on the unified document graph instead of the legacy layer graphs. But the incremental approach ended here— this last phase had to happen all at once, which posed a challenge for developing the editor while numerous features were fully broken pending rework. We began with a separate development branch, always kept up-to-date with the latest editor code changes, for a couple months until breakages were reduced to an acceptable level, then integrated with the main codebase in October. By tracking and burning down the list of 62 outstanding issues and regressions one-by-one, in mid-December we finally reached our long-sought goal: deploying a new stable release of Graphite featuring the unified node graph! I'd like to extend an extra big thank-you to core team member "Hypercube" for the dedication and persistence in grinding through most of these.

Block diagram of a Graphite document now that the migration is complete

Block diagram of the completed integration

I followed this up over my Christmas holiday by hunting down and ripping out over 6000 lines of unused code, satisfyingly bringing Graphite's total lines of Rust down to under 50,000— just about the same number as when we began, despite adding a year's worth of functionality! Fewer lines to understand and maintain makes our jobs easier, and I'm pleasantly surprised at how efficiently the Graphite team has managed to represent the app's considerable functionality in so relatively few lines. This is a sign of good engineering practices and it makes me proud of our capable team and what we have accomplished together.

Looking ahead to 2024

The hard part is done. The unified document node graph, now that it's complete, paves the pathway towards new feature development for vector and raster editing. The Alpha 2 release series is nearly ready to become Alpha 3 with a focus towards procedural editing as the theme for the year ahead. And hopefully the road to the Beta release series, and then 1.0, is not too much further ahead.

First, I have some high-level goals for 2024:

  • Cultivate a larger active community and grow the core team so we can move from a linear to an exponential pace of development
  • Begin sending quarterly email newsletters, publish these blog posts more frequently, and find a dedicated volunteer to assist in writing them while also growing Graphite's social media and internet presence
  • Announce Graphite to a wide audience and grow the daily active users by 10x or more, especially among artists
  • Reach 20,000 stars on GitHub (we just passed 5000 this December)
  • Move towards a greater focus on polish, stability, performance, and learning resources for the product
  • Attain sustainable income from donors and sponsors, and maybe even apply for grants in order to hire a full-time developer
  • Build infrastructure for user accounts and prove the viability of getting revenue from hosted cloud rendering

And then from a development perspective, I am looking forward to accomplishing these overarching objectives in 2024:

  • Restoring several previous features that were removed during refactors in the past year to a fully working state including snapping, folder bounding boxes, transform pivots, and vector shape boolean operations
  • Deploying GPU-based rendering by default and moving from an experimental to a production-ready hardware-accelerated compositing system using Vello to unify the currently separate raster and vector pipelines
  • Designing a new vector graphics data format suitable for advanced procedural editing and rendering, plus the associated procedural workflow features
  • Remaking the Brush tool with the GPU-accelerated pipeline and the adaptive resolution system so digital painting in Graphite becomes practical
  • Implementing the Mask Mode feature for Magic Wand tool marquee selections, which will dramatically improve Graphite's utility as a raster graphics editor
  • Supporting animation capabilities (a potential stretch goal for the year)

A call for community

Achieving everything listed above is ambitious, but it's ambition that has brought us to where we are today. Pulling this off will require a larger team and more resources than we've had in 2023. So if the mission we are striving for is exciting and you agree the world needs a truly great and versatile open source 2D graphics suite, we need your help!

  • Technically inclined developers interested in Rust, web dev, computer graphics, backend programming, compilers, machine learning, mathematics, or any of the other varied disciplines that Graphite overlaps with— we likely have a role or project for you.
  • We also have self-contained research projects involving problem-solving outside an existing codebase. One example: there are numerous industry-standard image filter effects we'd like to implement in Graphite where a volunteer could run analysis on the colors of test images to identify a suitable algorithm that gives matching results. We hope to assemble a larger Discord community of motivated people we can tap for help in solving these sorts of problems. These also make great university term projects and we've mentored several groups successfully in the past, so please reach out.
  • Technical artists with experience in procedural editing tools and engineers who enjoy designing solutions for complex problems would also be highly valuable community members when it comes to taking part in the many large, nuanced product and architecture design decisions we'll have to make this year.
  • Graphic designers and artists who put the time into using Graphite on a regular basis and helping us learn its practical strengths and weaknesses would also be valuable contributors. Helping with the creative parts of maintaining the app and assisting new users who have questions in the growing community would relieve the burden from the core team.
  • And spread the word! Create tutorial videos. Show off your creations on social media. Use it in your classroom. 2024 is the year Graphite is ready to come out of the shadows and get discovered.

If your New Year's resolution is joining an open source project, consider Graphite! We work hard to help new community contributors get up to speed with resources and guidance. We frequently hear praise that Graphite is a very inviting and supportive project from volunteers who have not had great prior experiences trying to get involved in open source. Join the Graphite Discord server and reach out to me (@Keavon) about how you'd like to get involved.

Launching our supporter fund

There's one last big way you can help and keep Graphite from needing to turn to investors who would someday come knocking for exponential profits at the expense of you, the user. That doesn't align with my vision so I have been self-funding Graphite for the past three years. But I, alone, can't keep that going for a fourth year and beyond.

With a laser-tight focus on completing our 2023 development objectives, I haven't yet called out for donations until now. You can be the very first person to join at the level of a Supporter (starting at $10 monthly) or Sponsor (starting at $50 monthly for individuals and $100 monthly for companies). Please consider joining at one of the levels listed here.

Please help launch Graphite towards the 2024 goal of attaining self-sufficiency so I can maintain my full-time commitment to an independent Graphite for the long haul. Thank you, it really means the world to me. ♥

Become a supporter

Wrapping up

Thank you to our community for an incredible year. Passing 5000 stars on the project GitHub repository was a wonderful gift this holiday season. Everyone who has given advice, written code, and expressed enthusiasm has inspired me every day to stay focused and motivated. It was especially a pleasure and honor to visit the many people and organizations mentioned in this post throughout the past year. The reception by all has been heartwarming and I look forward to staying connected with them and a growing Graphite community in the promising year that now lies ahead. I am most of all excited for the awesome state Graphite will be in when it comes time to write this post again next year.

Now go make some awesome art!