luster 0.1.0 copy "luster: ^0.1.0" to clipboard
luster: ^0.1.0 copied to clipboard

Mints SVG artwork into a lit 3D badge, rendered with flutter_scene. The engine is Luster's Rust core.

luster #

Strikes an SVG into a 3D gold enamel badge, and shows it lit and turning.

An SVG of a great wave on the left, and the enamel badge Luster strikes from it on the right, turning under studio lighting

Hand it a document and Luster builds the whole object from its paths — the silhouette, the enamel cells, the raised gold line work, the rolled edge, the sandblasted reverse — then draws it with flutter_scene or writes it out as GLB. It ships no 3D model assets and no artwork: everything comes from the document.

The engine is Rust, shared with Luster's Swift package and Kotlin library, so the same document gives the same badge, to the byte, on every platform.

Setup #

The widget draws with Flutter GPU, which the app has to turn on:

<!-- ios/Runner/Info.plist -->
<key>FLTEnableFlutterGPU</key>
<true/>
<!-- android/app/src/main/AndroidManifest.xml, inside <application> -->
<meta-data android:name="io.flutter.embedding.android.EnableFlutterGPU" android:value="true" />
<meta-data android:name="io.flutter.embedding.android.ImpellerBackend" android:value="vulkan" />

On Android Flutter GPU wants the Vulkan backend. The standard emulator images run Impeller on OpenGL ES, where the scene never comes up, so try it on a device.

The engine comes prebuilt for iOS, macOS and Android, and signed: the package checks each file against its key before using it, so the app needs no Rust. Where rustup is installed the engine is built from source instead, unless a cargokit_options.yaml at the app's root asks for the prebuilt one:

use_precompiled_binaries: true

Use #

import 'package:luster/luster.dart';

LusterView(
  source: LusterSource.url(Uri.parse('https://example.com/badge.svg')),
  options: const LusterOptions(metalLines: true),
  appearance: LusterAppearance(metal: LusterColor.silver),
  onStateChange: (state) => switch (state) {
    LusterReady(:final badge) => print(badge.designKey),
    LusterFailed(:final error) => print(error), // a LusterException, with its kind
    _ => null,
  },
)

Drag to turn the badge; it keeps going with momentum, unless the system asks for reduced motion or momentumEnabled is false. The widget paints nothing behind the badge: put it in a ColoredBox, or in a Stack over anything, for a background.

Minting runs on the engine's own threads, so the UI isolate keeps its frames, and the previous badge stays on screen while a new one is struck. A different source or options mints again; a different appearance only re-lights and re-plates.

What Type
The document LusterSource.url(uri), .bytes(bytes), .svg(text)
What to strike LusterOptions(metalLines:, withoutHiddenFaces:)
How to light it LusterAppearance(metal:, lighting:), with LusterColor.gold / silver / copper or any LusterColor, and LusterLighting.showcase / off
What it is doing LusterIdle, LusterMinting, LusterReady(badge), LusterFailed(error)

Luster.init() loads the engine and flutter_scene's shaders. The widget calls it itself; calling it at launch gets it done before the first badge is wanted. The named colours are the engine's, so they are there once it has loaded.

Without a view #

final badge = await Luster.mint(LusterSource.bytes(bytes),
    options: const LusterOptions(metalLines: true));
final glb = await badge.glb(metal: LusterColor.gold);          // glTF binary
final still = await LusterSnapshot.image(badge, pixelSize: 512); // ui.Image

At most two mints run at once across the process, and recent badges are kept, keyed by the document's bytes and the options, so asking again is immediate; asking for one already being struck waits for that mint rather than starting another. A badge's designKey names the document and the options it was struck with: equal keys mean the same badge.

A mint that fails throws a LusterException, whose kind says why: invalidSvg, inputTooComplex or nothingToMint for the document, tooLarge or unreadableSource for where it came from, engineFailure for a bug.

A list or a grid wants stills, not a view per row. A still is drawn as the view draws, and has no background unless it is given one: its alpha is the badge's coverage. background: takes a colour to lay it over instead.

badge.glb() writes the same bytes the other platforms write for the same badge and metal, so two exports can be compared directly.

From the web #

A web source is fetched with Luster.loader, by default dart:io's HttpClient, which keeps nothing; documents over 20 MB are refused, and on Android the app needs the INTERNET permission. To fetch through flutter_cache_manager, as cached_network_image does:

import 'package:flutter_cache_manager/flutter_cache_manager.dart';

Luster.loader = (url) async =>
    (await DefaultCacheManager().getSingleFile(url.toString())).readAsBytes();

A source that cannot be fetched fails with a LusterException whose kind is unreadableSource (tooLarge past the limit). Whatever the loader, the engine keeps recent badges by the document's bytes, so a document fetched again is not struck again.

How it is drawn #

The engine hands over the meshes and one sheet of colours, and the widget builds the scene in Dart with flutter_scene, not a platform view.

flutter_scene's tone curves keep a colour's saturation as it brightens, where RealityKit's let it fade towards white. The widget draws with no curve of its own and reads the result through RealityKit's, measured into a table, so a badge shows the colours it shows on the Apple side.

The badge is drawn every frame only while it turns; at rest it is drawn once, when something changes. Under the showcase it is drawn with its own shader (shaders/luster_badge.frag, compiled by hook/build.dart): flutter_scene's standard image-based lighting and nothing else, several times cheaper a pixel than the standard material. On a GPU too slow for full resolution the widget starts each turn coarser and comes down further while frames come too slowly.

The package pins the Flutter version it is built against in .fvmrc.

License #

MIT. See How it works for how the badge is made.

1
likes
160
points
12
downloads

Documentation

API reference

Publisher

verified publisherfamio.dev

Weekly Downloads

Mints SVG artwork into a lit 3D badge, rendered with flutter_scene. The engine is Luster's Rust core.

Repository (GitHub)
View/report issues
Contributing

Topics

#svg #graphics #badge #rendering #rust

License

MIT (license)

Dependencies

flutter, flutter_gpu_shaders, flutter_rust_bridge, flutter_scene, hooks, vector_math

More

Packages that depend on luster

Packages that implement luster