Skip to content

canvas: sampling/quality by render client — 'design' (see pixels) vs 'render' (best, =export & refig); ship remaining #867 painter fixes #12

Description

@softmarshmallow

Context

gridaco/grida#867 fixed five render-fidelity issues, split across two layers:

  • Converter (TypeScript) — Crop image fills (#897) + gradient/crop-image flip. Already shipped in @grida/refig@0.0.6 (#899); no wasm rebuild.
  • Rust painter (crates/grida, commit 3f03926fa) — layer opacity, image sampling, text line-height. Not yet on npm; reach @grida/refig only through a rebuilt @grida/canvas-wasm.

This issue tracks the Rust half. Opacity and line-height are clean correctness fixes; image sampling carries an open design question (below) and should NOT be shipped as the current hardcode.

Clean fixes — release with the wasm rebuild (correctness, no tradeoff)

  • Solid opacity (paint.rs) — per-fill alpha was squared (color.a * opacity); now uses the color alpha directly.
  • Line-height leading (text_style.rs) — switched to half-leading (CSS/Figma model).

Image sampling — drive it from the render client, not a hardcode

gridaco/grida#867 hardcoded SamplingOptions::from(CubicResampler::mitchell()) at crates/grida/src/painter/image.rs:48. image_shader (called from painter/paint.rs:150) takes no context, so every path gets identical sampling. The fix is to make sampling a function of which of two clients is rendering:

Client A — design (interactive canvas). The designer is inspecting the artwork and wants to see the actual image pixels — pixel-faithful, cheap sampling (Nearest). Truthful to the source texels, and fast for live pan/zoom.

Client B — render (output / best). Produce the best possible image — high-quality resampling (Mitchell cubic).

Which client runs when:

  • The design canvas renders as design while editing, and switches to render when the user exports.
  • refig is always render — headless rendering is output by definition.

So the same scene samples differently depending on the client carried in the render context. Exact enum naming TBD (e.g. RenderMode { Design, Render } / RenderIntent { Edit, Export }).

Orthogonal sub-axis: a per-image content hint (smooth vs crisp / pixel-art, à la CSS image-rendering) can layer on top within a client — precedent in the htmlcss path: sampling_for(ImageRendering) at crates/grida/src/htmlcss/paint.rs:904 (Auto→Linear, pixelated→Nearest).

Prior art — gridaco/grida#509 Pixel Preview (build on this, don't reinvent)

gridaco/grida#509 already introduced the "design client sees pixels" mode and the infrastructure to drive it:

  • WASM API runtime_renderer_set_pixel_preview_scale(0|1|2) + runtime_renderer_set_pixel_preview_stable(bool) (crates/grida-canvas-wasm/src/wasm_application.rs).
  • Backed by FrameRenderStrategy (crates/grida/src/runtime/frame_strategy.rs) — the centralized per-frame render-policy struct, computed once per frame and consulted at each decision point. It already encodes a quality-vs-perf tradeoff (the "blurry during gesture, sharp on settle" cache gating, EAGER_RENDER_ZOOM_THRESHOLD).

FrameRenderStrategy::compute() is the natural home for the render-client / sampling decision — add it as a policy field there and have image_shader read it, instead of the bare constant at image.rs:48. The designrender client distinction generalizes the existing pixel-preview stable/scale knobs.

The test image_shader_samples_smoothly_not_nearest (painter/image.rs) asserts today's blanket smooth default; it moves onto the new surface (assert render smooths, design shows pixels).

Until this exists, shipping the gridaco/grida#867 Mitchell hardcode to the interactive canvas both costs pan/zoom perf and hides the real pixels from the designer.

Release order (maintainer, npm publish rights)

  1. Get fix(refig): Figma REST render fidelity (opacity, image CROP, gradient flip, sampling, line-height) grida#867 into the wasm-build source — merge main → canary, or build wasm locally from main.
  2. Resolve the render-client / sampling design above — or knowingly ship the Mitchell hardcode as an interim (both clients smooth) and keep this issue open for the client work.
  3. Rebuild + republish @grida/canvas-wasm as a new canary (e.g. canary.22).
  4. Repin @grida/refig → new canary; version + publish (CHANGELOG adds opacity / sampling / line-height).

Acceptance

  • A render-client / quality mode exists in the render context: design (see pixels / cheap) vs render (best), threaded via FrameRenderStrategy
  • design canvas renders as design interactively, switches to render on export; refig is always render
  • per-image smooth/crisp content hint honored within a client (pixel-art opt-in)
  • @grida/canvas-wasm canary rebuilt with fix(refig): Figma REST render fidelity (opacity, image CROP, gradient flip, sampling, line-height) grida#867 painter changes
  • @grida/refig repinned + republished; opacity / sampling / line-height verified against a Figma oracle

Refs: gridaco/grida#867, gridaco/grida#897, gridaco/grida#899, gridaco/grida#509 (Pixel Preview — prior art), gridaco/grida#898 (publish CI)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions