zaniah 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/docs/adr/005-instance-layout.md +4 -4
- data/docs/adr/016-semantic-vector-recording.md +33 -0
- data/docs/adr/017-analytic-shadows.md +31 -0
- data/docs/adr/018-gradient-ramps.md +30 -0
- data/docs/vector.md +60 -0
- data/docs/vector_and_list.md +13 -4
- data/examples/metal_effect_check.rb +30 -0
- data/lib/zaniah/gpu/instance_packing.rb +2 -0
- data/lib/zaniah/gpu/metal.rb +27 -5
- data/lib/zaniah/gpu/open_gl.rb +29 -6
- data/lib/zaniah/gpu/software.rb +58 -6
- data/lib/zaniah/gpu/vulkan/scene.frag +26 -4
- data/lib/zaniah/gpu/vulkan/scene.frag.spv +0 -0
- data/lib/zaniah/gpu/vulkan/scene.vert +2 -0
- data/lib/zaniah/gpu/vulkan/scene.vert.spv +0 -0
- data/lib/zaniah/image.rb +2 -2
- data/lib/zaniah/platform/headless/window.rb +1 -0
- data/lib/zaniah/platform/tui/window.rb +27 -0
- data/lib/zaniah/scene.rb +167 -27
- data/lib/zaniah/style/gradient.rb +6 -1
- data/lib/zaniah/svg/rasterizable.rb +54 -0
- data/lib/zaniah/svg.rb +157 -14
- data/lib/zaniah/text_system/renderer.rb +31 -1
- data/lib/zaniah/vector.rb +63 -0
- data/lib/zaniah/version.rb +1 -1
- data/sig/scene.rbs +9 -1
- data/sig/vector.rbs +116 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc06ae1720a8d05abbc0da78849a355f7c36565c62b3907f941c80dd066594c5
|
|
4
|
+
data.tar.gz: fd00f6d9bd08c097a5ffa35ce079a83f949c07874d6ff7c76cdc802f7ddd8129
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 769bfa73b348d773f2139eb34039a7250f6878986375bdd25c361c3922b16d61ab68a3d14c59f8ef7f3cfd68e04a43d868286b624d1996d2fc2afdb92b23f861
|
|
7
|
+
data.tar.gz: a9642fa580bd9595e15c48903e698b95dfc5a6f876ca0d71438e488899522e2855351cef18a124e005dae5636e53b500aa46e90e101575c12796fc4e9ae80992
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 — 2026-09-24
|
|
4
|
+
|
|
5
|
+
- Add semantic vector recording for UI scenes, including text, images, and raster fallbacks for unsupported drawing.
|
|
6
|
+
- Render shadows analytically across backends; their edge appearance changes from earlier releases.
|
|
7
|
+
- Add shared multi-stop and conic gradient ramps across Software, OpenGL, Metal, and Vulkan rendering.
|
|
8
|
+
- Support SVG gradients, dashed strokes, and alpha and luminance masks.
|
|
9
|
+
|
|
3
10
|
## 0.8.0 — 2026-09-24
|
|
4
11
|
|
|
5
12
|
- Add native macOS and Windows menus, declarative context menus, and platform-aware shortcut labels.
|
|
@@ -26,10 +26,10 @@ fields for source UVs. Triangle vertices reuse rect and radii fields.
|
|
|
26
26
|
| 32–37 | affine transform (`a,b,c,d,tx,ty`) | affine transform |
|
|
27
27
|
| 38–39 | reserved shadow spread / inset or border flags | unused |
|
|
28
28
|
|
|
29
|
-
Opacity is multiplied into both fill and border alpha before packing.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
Opacity is multiplied into both fill and border alpha before packing. The
|
|
30
|
+
original two-stop and expanded-quad behavior has since been extended without
|
|
31
|
+
changing this layout: see [ADR 017](017-analytic-shadows.md) for the shadow
|
|
32
|
+
fields and [ADR 018](018-gradient-ramps.md) for multistop ramps and conic fills.
|
|
33
33
|
|
|
34
34
|
Metal, OpenGL, and Vulkan consume the layout directly, Software consumes the
|
|
35
35
|
equivalent 40-float quad layout, and cached text sprite batches use the same
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# ADR 016: Record semantic drawing alongside the GPU scene
|
|
2
|
+
|
|
3
|
+
- Status: Proposed
|
|
4
|
+
- Date: 2026-09-24
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
The GPU scene contains packed quads and atlas sprites. Paths have already been
|
|
9
|
+
rasterized and text has lost its font, glyph IDs, and source clusters by the
|
|
10
|
+
time a renderer consumes those commands. Exporters need this information to
|
|
11
|
+
produce searchable text and scalable artwork. Reconstructing it from packed
|
|
12
|
+
GPU instances is impossible; maintaining a second application-specific layout
|
|
13
|
+
for export makes screen and export drift.
|
|
14
|
+
|
|
15
|
+
Two viable boundaries were considered: make the GPU scene itself semantic, or
|
|
16
|
+
keep its fast packed representation and optionally observe the drawing calls.
|
|
17
|
+
|
|
18
|
+
## Decision
|
|
19
|
+
|
|
20
|
+
Keep the GPU scene unchanged and attach an optional vector sink to `Scene`.
|
|
21
|
+
Emit semantic commands before paths and text are flattened. When a source has
|
|
22
|
+
no supported vector form, preserve its pixels as a raster command instead.
|
|
23
|
+
The sink is absent by default, so normal frames do not allocate vector data.
|
|
24
|
+
Page assembly and document formats remain outside Zaniah.
|
|
25
|
+
|
|
26
|
+
## Consequences
|
|
27
|
+
|
|
28
|
+
Exporters can reuse the same element tree and retain font and outline data
|
|
29
|
+
without adding a dependency on a PDF library. The optional path incurs a
|
|
30
|
+
nil check when unused and stores additional frame data when enabled. Raster
|
|
31
|
+
fallbacks keep output complete but cannot scale as cleanly as true vectors.
|
|
32
|
+
This choice should be revisited if packed GPU commands become a lossless
|
|
33
|
+
semantic representation or a common display list can serve both consumers.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# ADR 017: Draw each shadow as one analytic instance
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-09-24
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
The previous shadow approximation layered eight rounded quads. It produced
|
|
9
|
+
visible bands and made a scene with 100 shadows submit 800 instances.
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
Keep the 40-float instance layout from ADR 005. Primitive kind `4` in field 31
|
|
14
|
+
is a shadow; field 30 is blur sigma, field 38 is spread, and field 39 is the
|
|
15
|
+
inset flag. Bounds 0–3 are expanded by `spread + 3 * blur + 1` for outer
|
|
16
|
+
shadows. Inset shadows retain the original bounds. Corner radii occupy 12–15.
|
|
17
|
+
|
|
18
|
+
Software and all three GPU fragment shaders evaluate the signed distance to
|
|
19
|
+
the original rounded rectangle. A Gaussian edge is approximated by
|
|
20
|
+
`0.5 - 0.5 * erf((distance - spread) / (sqrt(2) * blur))`, using the same bounded
|
|
21
|
+
exponential approximation of `erf` in every backend. Zero blur uses direct
|
|
22
|
+
coverage. Inset shadows multiply interior coverage by the inverted transition.
|
|
23
|
+
The existing clipping, transforms, and alpha blending remain unchanged.
|
|
24
|
+
|
|
25
|
+
## Consequences
|
|
26
|
+
|
|
27
|
+
Shadow appearance intentionally changes; the golden images and changelog must
|
|
28
|
+
record this. `bench/shadow.rb` compares 100 shadows against the old eight-quad
|
|
29
|
+
approximation and requires the analytic Software path to be faster. GPU native
|
|
30
|
+
capture tests remain platform-dependent; `script/compile_shaders` regenerates
|
|
31
|
+
the bundled Vulkan SPIR-V from the checked-in GLSL.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ADR 018: Use bounded ramps for multistop gradients
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-09-24
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
The shared instance format carries only two inline stop colors. Extending its
|
|
9
|
+
stride would affect every renderer and text/sprite packing.
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
Two-stop linear and radial gradients retain their inline color path. Two-stop
|
|
14
|
+
conic gradients use kind `3` and the same interpolation path. Three or more
|
|
15
|
+
stops use kinds `4` (linear), `5` (radial), or `6` (conic), and one row of a
|
|
16
|
+
256-by-256 RGBA8 atlas baked from the ordered stops. Field 8 stores the row
|
|
17
|
+
index; all renderers sample that row at the calculated gradient position. A
|
|
18
|
+
`Scene` caches rows by immutable `Gradient` value in an LRU capped at 256
|
|
19
|
+
entries. Rows used in the current frame cannot be evicted; if a frame exceeds
|
|
20
|
+
256 distinct ramps, it uses reusable overflow atlases for the remainder of
|
|
21
|
+
that frame. Normal quads continue to use packed-byte fast paths; only ramp
|
|
22
|
+
quads need texture-aware batching.
|
|
23
|
+
|
|
24
|
+
## Consequences
|
|
25
|
+
|
|
26
|
+
No instance layout migration is required. Sampling precision is limited to
|
|
27
|
+
256 positions. Adjacent ramps on the shared atlas batch together, while
|
|
28
|
+
overflows can add batches. SVG gradient fills use the same ordered-stop
|
|
29
|
+
interpolation semantics and fall back to a raster snapshot in vector recording
|
|
30
|
+
because a `Vector::Path` currently stores solid fills only.
|
data/docs/vector.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Vector recording
|
|
2
|
+
|
|
3
|
+
Zaniah records the drawing it already performs without taking ownership of PDF
|
|
4
|
+
pages, paper sizes, or export dialogs. Require `zaniah/vector` and render a normal
|
|
5
|
+
element tree offscreen:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
document = Zaniah::Vector.record(width: 960, height: 540,
|
|
9
|
+
theme: Zaniah::Theme.light) { slide }
|
|
10
|
+
document.commands.each { |command| export_command(command) }
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
To inspect a displayed frame, attach a recorder before rendering. `Scene#clear`
|
|
14
|
+
resets its commands each frame; save `recorder.document` before the next frame.
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
recorder = Zaniah::Vector::Recorder.new
|
|
18
|
+
window.scene.vector_sink = recorder
|
|
19
|
+
window.tick
|
|
20
|
+
document = recorder.document
|
|
21
|
+
window.scene.vector_sink = nil
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`Document#width` and `#height` are logical pixels. A `Recorder` attached to a
|
|
25
|
+
window receives that window's content size; a recorder attached to a bare
|
|
26
|
+
`Scene` may instead be initialized with `width:` and `height:`. Coordinates use
|
|
27
|
+
a top-left origin and downward y axis; colors are sRGB. Every command carries
|
|
28
|
+
its transform, clip, opacity, layer, and sequence. `Document#commands` is sorted
|
|
29
|
+
by layer and then sequence, matching `Scene#each_command`.
|
|
30
|
+
|
|
31
|
+
| Command | Export information |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `Quad` | Bounds, fill color or gradient, corner radii, border widths/color/style |
|
|
34
|
+
| `Shadow` | Bounds, corner radii, color, blur, spread, inset |
|
|
35
|
+
| `Path` | Alhena outline, fill, stroke, stroke width, fill rule and stroke cap/join |
|
|
36
|
+
| `GlyphRun` | Alhena font, size, `[glyph_id, x, baseline_y]` triples, original text and UTF-8 byte clusters |
|
|
37
|
+
| `Image` | Decoded `Zaniah::Image`, bounds and source rectangle, plus a frozen pixel snapshot |
|
|
38
|
+
| `Underline` | Position, thickness and wave flag |
|
|
39
|
+
| `Raster` | Frozen source texture bytes, pixel dimensions/format, tint and source rectangle |
|
|
40
|
+
|
|
41
|
+
Ordinary sprites and packed sprite batches become `Raster`; bitmap-color text
|
|
42
|
+
uses that fallback for the whole line. Simple SVG fills and strokes become
|
|
43
|
+
`Path` commands. SVG clipping, gradients, dash arrays, masks, group opacity,
|
|
44
|
+
and references use a `Raster`
|
|
45
|
+
fallback, so they are not silently omitted. The `Raster` bytes are a snapshot
|
|
46
|
+
at recording time, even if the source texture changes later. The converter must
|
|
47
|
+
crop to `source`, apply `color` and `opacity`, then respect `transform` and
|
|
48
|
+
`clip`. It may rasterize a `Path` or `GlyphRun` when its output format lacks
|
|
49
|
+
an equivalent vector primitive.
|
|
50
|
+
|
|
51
|
+
The headless offscreen API works on every OS and does not require a GPU.
|
|
52
|
+
Attaching a recorder to macOS, Windows, X11, or Wayland uses the same `Scene`
|
|
53
|
+
path. The TUI backend captures its terminal cells as a single `Raster` fallback
|
|
54
|
+
at an 8-by-20 cell grid, using a bundled font approximation; terminal font
|
|
55
|
+
appearance is outside Zaniah's control. Use the offscreen API when scalable
|
|
56
|
+
text is required.
|
|
57
|
+
|
|
58
|
+
See [ADR 016](adr/016-semantic-vector-recording.md) for why this is separate
|
|
59
|
+
from the GPU command stream. [Vector and list](vector_and_list.md) documents
|
|
60
|
+
the SVG input subset.
|
data/docs/vector_and_list.md
CHANGED
|
@@ -13,15 +13,24 @@ icon = Zaniah::SVG.parse(
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
The renderer supports SVG paths, basic shapes, groups, transforms, view boxes,
|
|
16
|
-
inherited fill and stroke, `currentColor`, opacity, local `defs`/`use`,
|
|
17
|
-
user-space clip paths
|
|
16
|
+
inherited fill and stroke, `currentColor`, opacity, local `defs`/`use`,
|
|
17
|
+
user-space clip paths, linear/radial gradients, stroke dash arrays and offsets,
|
|
18
|
+
and local alpha/luminance masks. It is intended for static icons, not arbitrary
|
|
19
|
+
web SVG.
|
|
18
20
|
|
|
19
|
-
Scripts, external references, CSS stylesheets,
|
|
20
|
-
text, markers,
|
|
21
|
+
Scripts, external references, CSS stylesheets, filters, images,
|
|
22
|
+
text, markers, nested viewports, and object-bounding-box clips are
|
|
21
23
|
unsupported and raise `ArgumentError`. Input is bounded to 2 MiB, 10,000 XML
|
|
22
24
|
nodes, 64 levels, and 100,000 path operations; each output texture is limited to
|
|
23
25
|
one megapixel.
|
|
24
26
|
|
|
27
|
+
SVG gradients support ordered stops, local references, `objectBoundingBox` and
|
|
28
|
+
`userSpaceOnUse` coordinates, and the default pad spread. Other spread methods
|
|
29
|
+
and external references remain unsupported. Masks rasterize within the same
|
|
30
|
+
one-megapixel output limit. Gradient, dashed, and masked shapes are represented
|
|
31
|
+
as a `Vector::Raster` fallback when recording; simple solid outlines remain
|
|
32
|
+
`Vector::Path` commands.
|
|
33
|
+
|
|
25
34
|
## Variable-height lists
|
|
26
35
|
|
|
27
36
|
```ruby
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../lib/zaniah"
|
|
4
|
+
abort "This check needs macOS and a graphical login" unless RUBY_PLATFORM.include?("darwin")
|
|
5
|
+
|
|
6
|
+
window = Zaniah::Platform.open_window(backend: :mac, gpu: :metal, width: 96, height: 72,
|
|
7
|
+
title: "Metal effect check")
|
|
8
|
+
begin
|
|
9
|
+
scene = Zaniah::Scene.new
|
|
10
|
+
scene.quad(4, 4, 30, 24, color: Zaniah::Gradient.linear(angle: 0,
|
|
11
|
+
stops: [[0, "#f00"], [0.5, "#0f0"], [1, "#00f"]]))
|
|
12
|
+
scene.quad(38, 4, 30, 24, color: Zaniah::Gradient.conic(stops: [[0, "#f00"], [1, "#00f"]]))
|
|
13
|
+
scene.shadow(28, 40, 32, 18, color: "#000", blur: 3, spread: 1, radius: 4)
|
|
14
|
+
expected = Zaniah::GPU::Software.new(96, 72).render(scene, clear: "#fff")
|
|
15
|
+
window.device.render(scene, clear: "#fff")
|
|
16
|
+
actual = window.device.pixels
|
|
17
|
+
scale = window.scale_factor
|
|
18
|
+
pixel_width = (96 * scale).round
|
|
19
|
+
samples = [[6, 15], [18, 15], [31, 15], [60, 16], [22, 49], [28, 49], [44, 49], [75, 49]]
|
|
20
|
+
samples.each do |x, y|
|
|
21
|
+
index = (y * 96 + x) * 4
|
|
22
|
+
native_index = (((y + 0.5) * scale).floor * pixel_width + ((x + 0.5) * scale).floor) * 4
|
|
23
|
+
left, right = expected.byteslice(index, 4).bytes, actual.byteslice(native_index, 4).bytes
|
|
24
|
+
raise "effect mismatch at #{x},#{y}: #{right.inspect} != #{left.inspect}" unless
|
|
25
|
+
left.zip(right).all? { |a, b| (a - b).abs <= 8 }
|
|
26
|
+
end
|
|
27
|
+
puts "Metal analytic shadow and gradient check: #{samples.length} samples passed"
|
|
28
|
+
ensure
|
|
29
|
+
window.close
|
|
30
|
+
end
|
|
@@ -17,6 +17,7 @@ module Zaniah
|
|
|
17
17
|
case kind
|
|
18
18
|
when :quad
|
|
19
19
|
q, i = scene.quads, offset
|
|
20
|
+
texture = scene.quad_texture(offset)
|
|
20
21
|
data.push(q[i], q[i+1], q[i+2], q[i+3], q[i+4], q[i+5], q[i+6], q[i+7],
|
|
21
22
|
q[i+8], q[i+9], q[i+10], q[i+11], q[i+12], q[i+13], q[i+14], q[i+15],
|
|
22
23
|
q[i+16], q[i+17], q[i+18], q[i+19], q[i+20], q[i+21], q[i+22], q[i+23],
|
|
@@ -64,6 +65,7 @@ module Zaniah
|
|
|
64
65
|
expected = 0
|
|
65
66
|
scene.each_command do |kind, offset, clip|
|
|
66
67
|
return unless kind == :quad && offset == expected
|
|
68
|
+
return if scene.quad_texture(offset)
|
|
67
69
|
if batches.empty? || batches.last[0][2] != clip
|
|
68
70
|
batches << [[:quad, nil, clip], expected / STRIDE, 1]
|
|
69
71
|
else
|
data/lib/zaniah/gpu/metal.rb
CHANGED
|
@@ -20,7 +20,7 @@ module Zaniah
|
|
|
20
20
|
using namespace metal;
|
|
21
21
|
struct Vertex { float4 position [[position]]; float2 local; float2 size;
|
|
22
22
|
float4 color; float4 secondary; float4 radii; float4 border; float4 widths;
|
|
23
|
-
float4 gradient; float4 center_kind; float2 uv; float dash; };
|
|
23
|
+
float4 gradient; float4 center_kind; float2 uv; float dash; float spread; };
|
|
24
24
|
vertex Vertex zaniah_vertex(uint v [[vertex_id]], uint instance [[instance_id]],
|
|
25
25
|
const device float *data [[buffer(0)]], constant float2 &viewport [[buffer(1)]]) {
|
|
26
26
|
const device float *p = data + instance * 40;
|
|
@@ -37,17 +37,26 @@ module Zaniah
|
|
|
37
37
|
out.radii = float4(p[12],p[13],p[14],p[15]); out.border = float4(p[16],p[17],p[18],p[19]);
|
|
38
38
|
out.widths = float4(p[20],p[21],p[22],p[23]); out.gradient = float4(p[24],p[25],p[26],p[27]);
|
|
39
39
|
out.center_kind = float4(p[28],p[29],p[30],p[31]);
|
|
40
|
-
out.uv = float2(p[20],p[21]) + corner * float2(p[22],p[23]); out.dash = p[39]; return out;
|
|
40
|
+
out.uv = float2(p[20],p[21]) + corner * float2(p[22],p[23]); out.dash = p[39]; out.spread = p[38]; return out;
|
|
41
|
+
}
|
|
42
|
+
float approx_erf(float value) {
|
|
43
|
+
float square = value * value;
|
|
44
|
+
return sign(value) * sqrt(1.0f - exp(-square * (1.27323954474f + 0.147f * square) / (1.0f + 0.147f * square)));
|
|
41
45
|
}
|
|
42
46
|
fragment float4 zaniah_fragment(Vertex in [[stage_in]], texture2d<float> atlas [[texture(0)]]) {
|
|
43
47
|
float4 color = in.color;
|
|
44
48
|
if (in.center_kind.w == 0) {
|
|
45
49
|
if (in.gradient.x > 0) {
|
|
46
50
|
float2 normalized = in.local / in.size;
|
|
47
|
-
float raw = in.gradient.x == 1
|
|
51
|
+
float raw = in.gradient.x == 1 || in.gradient.x == 4
|
|
48
52
|
? dot(normalized - 0.5f, float2(cos(in.gradient.w * 0.01745329252f), sin(in.gradient.w * 0.01745329252f))) + 0.5f
|
|
49
|
-
: length(normalized - in.center_kind.xy) / in.center_kind.z
|
|
50
|
-
|
|
53
|
+
: in.gradient.x == 2 || in.gradient.x == 5 ? length(normalized - in.center_kind.xy) / in.center_kind.z
|
|
54
|
+
: fract((atan2(normalized.y - in.center_kind.y, normalized.x - in.center_kind.x) - in.gradient.w * 0.01745329252f) / 6.28318530718f + 1.0f);
|
|
55
|
+
float amount = clamp((raw - in.gradient.y) / max(in.gradient.z - in.gradient.y, 0.000001f), 0.0f, 1.0f);
|
|
56
|
+
constexpr sampler gradient_sampler(coord::normalized, address::clamp_to_edge, filter::nearest);
|
|
57
|
+
float2 ramp_uv = float2((floor(amount * 255.0f + 0.5f) + 0.5f) / 256.0f, (in.secondary.x + 0.5f) / 256.0f);
|
|
58
|
+
color = in.gradient.x >= 4 ? atlas.sample(gradient_sampler, ramp_uv) * float4(1.0f,1.0f,1.0f,in.color.a)
|
|
59
|
+
: mix(in.color, in.secondary, amount);
|
|
51
60
|
}
|
|
52
61
|
float radius = in.local.y < in.size.y/2 ? (in.local.x < in.size.x/2 ? in.radii.x : in.radii.y) : (in.local.x < in.size.x/2 ? in.radii.w : in.radii.z);
|
|
53
62
|
radius = clamp(radius, 0.0f, min(in.size.x,in.size.y)/2);
|
|
@@ -60,6 +69,19 @@ module Zaniah
|
|
|
60
69
|
float coordinate = edge == edge_distance.x || edge == edge_distance.z ? in.local.x : in.local.y;
|
|
61
70
|
if (border_width > 0 && distance >= -border_width && !(in.dash == 1 && fmod(coordinate,6.0f) >= 3.0f)) color = in.border;
|
|
62
71
|
color.a *= coverage;
|
|
72
|
+
} else if (in.center_kind.w == 4) {
|
|
73
|
+
float margin = in.dash == 1 ? 0.0f : in.spread + in.center_kind.z * 3.0f + 1.0f;
|
|
74
|
+
float2 original_size = in.size - 2.0f * margin;
|
|
75
|
+
float2 p = in.local - margin;
|
|
76
|
+
float radius = p.y < original_size.y/2.0f ? (p.x < original_size.x/2.0f ? in.radii.x : in.radii.y) : (p.x < original_size.x/2.0f ? in.radii.w : in.radii.z);
|
|
77
|
+
radius = clamp(radius, 0.0f, min(original_size.x,original_size.y)/2.0f);
|
|
78
|
+
float2 q = abs(p - original_size/2.0f) - original_size/2.0f + radius;
|
|
79
|
+
float distance = length(max(q,0.0f)) + min(max(q.x,q.y),0.0f) - radius;
|
|
80
|
+
float sigma = in.center_kind.z;
|
|
81
|
+
float coverage = in.dash == 1
|
|
82
|
+
? clamp(0.5f - distance, 0.0f, 1.0f) * (sigma == 0.0f ? clamp(distance + in.spread + 0.5f, 0.0f, 1.0f) : 0.5f + 0.5f * approx_erf((distance + in.spread) / (sigma * 1.41421356237f)))
|
|
83
|
+
: sigma == 0.0f ? clamp(0.5f - distance + in.spread, 0.0f, 1.0f) : 0.5f - 0.5f * approx_erf((distance - in.spread) / (sigma * 1.41421356237f));
|
|
84
|
+
color.a *= coverage;
|
|
63
85
|
} else if (in.center_kind.w == 1 || in.center_kind.w == 2) {
|
|
64
86
|
constexpr sampler texture_sampler(coord::normalized, address::clamp_to_edge, filter::nearest);
|
|
65
87
|
float4 sample = atlas.sample(texture_sampler,in.uv);
|
data/lib/zaniah/gpu/open_gl.rb
CHANGED
|
@@ -23,7 +23,7 @@ module Zaniah
|
|
|
23
23
|
uniform vec2 viewport;
|
|
24
24
|
out vec2 local_position; out vec2 size; out vec4 color; out vec4 radii;
|
|
25
25
|
out vec4 secondary; out vec4 border; out vec4 widths; out vec4 gradient;
|
|
26
|
-
out vec4 center_kind; out vec2 uv; out float dash;
|
|
26
|
+
out vec4 center_kind; out vec2 uv; out float dash; out float spread;
|
|
27
27
|
void main() {
|
|
28
28
|
vec2 vertices[4] = vec2[4](vec2(0,0),vec2(1,0),vec2(0,1),vec2(1,1));
|
|
29
29
|
vec2 corner = vertices[gl_VertexID];
|
|
@@ -35,25 +35,35 @@ module Zaniah
|
|
|
35
35
|
local_position = corner * rect.zw; size = rect.zw; color = tint;
|
|
36
36
|
secondary = tint2; radii = corners; border = border_color; widths = edges_source;
|
|
37
37
|
gradient = gradient_data; center_kind = gradient_center_kind;
|
|
38
|
-
uv = edges_source.xy + corner * edges_source.zw; dash = translation_flags.w;
|
|
38
|
+
uv = edges_source.xy + corner * edges_source.zw; dash = translation_flags.w; spread = translation_flags.z;
|
|
39
39
|
}
|
|
40
40
|
GLSL
|
|
41
41
|
FRAGMENT = <<~GLSL.freeze
|
|
42
42
|
#version 330 core
|
|
43
43
|
in vec2 local_position; in vec2 size; in vec4 color; in vec4 radii;
|
|
44
44
|
in vec4 secondary; in vec4 border; in vec4 widths; in vec4 gradient;
|
|
45
|
-
in vec4 center_kind; in vec2 uv; in float dash;
|
|
45
|
+
in vec4 center_kind; in vec2 uv; in float dash; in float spread;
|
|
46
46
|
uniform sampler2D atlas;
|
|
47
47
|
out vec4 output_color;
|
|
48
|
+
float approx_erf(float value) {
|
|
49
|
+
float square = value * value;
|
|
50
|
+
return sign(value) * sqrt(1 - exp(-square * (1.27323954474 + 0.147 * square) / (1 + 0.147 * square)));
|
|
51
|
+
}
|
|
48
52
|
void main() {
|
|
49
53
|
vec4 result = color;
|
|
50
54
|
if (center_kind.w == 0) {
|
|
51
55
|
if (gradient.x > 0) {
|
|
52
56
|
vec2 normalized = local_position / size;
|
|
53
|
-
float raw = gradient.x == 1
|
|
57
|
+
float raw = gradient.x == 1 || gradient.x == 4
|
|
54
58
|
? dot(normalized - 0.5, vec2(cos(radians(gradient.w)), sin(radians(gradient.w)))) + 0.5
|
|
55
|
-
: length(normalized - center_kind.xy) / center_kind.z
|
|
56
|
-
|
|
59
|
+
: gradient.x == 2 || gradient.x == 5 ? length(normalized - center_kind.xy) / center_kind.z
|
|
60
|
+
: fract((atan(normalized.y - center_kind.y, normalized.x - center_kind.x) - radians(gradient.w)) / 6.28318530718 + 1.0);
|
|
61
|
+
float amount = clamp((raw - gradient.y) / max(gradient.z - gradient.y, 0.000001), 0, 1);
|
|
62
|
+
if (gradient.x >= 4) {
|
|
63
|
+
vec2 ramp_uv = vec2((floor(amount * 255.0 + 0.5) + 0.5) / 256.0, (secondary.x + 0.5) / 256.0);
|
|
64
|
+
vec4 sample_color = texture(atlas, ramp_uv);
|
|
65
|
+
result = vec4(sample_color.rgb, sample_color.a * color.a);
|
|
66
|
+
} else result = mix(color, secondary, amount);
|
|
57
67
|
}
|
|
58
68
|
float radius = local_position.y < size.y/2 ? (local_position.x < size.x/2 ? radii.x : radii.y) : (local_position.x < size.x/2 ? radii.w : radii.z);
|
|
59
69
|
radius = clamp(radius, 0, min(size.x,size.y)/2);
|
|
@@ -66,6 +76,19 @@ module Zaniah
|
|
|
66
76
|
float coordinate = edge == edge_distance.x || edge == edge_distance.z ? local_position.x : local_position.y;
|
|
67
77
|
if (border_width > 0 && distance >= -border_width && !(dash == 1 && mod(coordinate,6) >= 3)) result = border;
|
|
68
78
|
result.a *= coverage;
|
|
79
|
+
} else if (center_kind.w == 4) {
|
|
80
|
+
float margin = dash == 1 ? 0 : spread + center_kind.z * 3 + 1;
|
|
81
|
+
vec2 original_size = size - 2 * margin;
|
|
82
|
+
vec2 p = local_position - margin;
|
|
83
|
+
float radius = p.y < original_size.y/2 ? (p.x < original_size.x/2 ? radii.x : radii.y) : (p.x < original_size.x/2 ? radii.w : radii.z);
|
|
84
|
+
radius = clamp(radius, 0, min(original_size.x,original_size.y)/2);
|
|
85
|
+
vec2 q = abs(p - original_size/2) - original_size/2 + radius;
|
|
86
|
+
float distance = length(max(q,0)) + min(max(q.x,q.y),0) - radius;
|
|
87
|
+
float sigma = center_kind.z;
|
|
88
|
+
float coverage = dash == 1
|
|
89
|
+
? clamp(0.5 - distance, 0, 1) * (sigma == 0 ? clamp(distance + spread + 0.5, 0, 1) : 0.5 + 0.5 * approx_erf((distance + spread) / (sigma * 1.41421356237)))
|
|
90
|
+
: sigma == 0 ? clamp(0.5 - distance + spread, 0, 1) : 0.5 - 0.5 * approx_erf((distance - spread) / (sigma * 1.41421356237));
|
|
91
|
+
result.a *= coverage;
|
|
69
92
|
} else if (center_kind.w == 1 || center_kind.w == 2) {
|
|
70
93
|
vec4 sample_color = texture(atlas,uv);
|
|
71
94
|
if (center_kind.w == 1) result.a *= sample_color.r; else result *= sample_color;
|
data/lib/zaniah/gpu/software.rb
CHANGED
|
@@ -27,7 +27,7 @@ module Zaniah
|
|
|
27
27
|
scene.each_command do |kind, offset, clip|
|
|
28
28
|
bounds = clip ? @viewport.intersect(clip) : @viewport
|
|
29
29
|
case kind
|
|
30
|
-
when :quad then draw_quad(scene
|
|
30
|
+
when :quad then draw_quad(scene, offset, bounds)
|
|
31
31
|
when :sprite then draw_sprite(scene, offset, bounds)
|
|
32
32
|
when :sprite_batch
|
|
33
33
|
first, count = scene.expand_sprite_batch(offset)
|
|
@@ -43,8 +43,9 @@ module Zaniah
|
|
|
43
43
|
|
|
44
44
|
private
|
|
45
45
|
|
|
46
|
-
def draw_quad(
|
|
47
|
-
values =
|
|
46
|
+
def draw_quad(scene, offset, clip)
|
|
47
|
+
values = scene.quads.slice(offset, Scene::QUAD_STRIDE)
|
|
48
|
+
return draw_shadow(values, clip) if values[31] == 4
|
|
48
49
|
x, y, width, height = values.first(4)
|
|
49
50
|
color, secondary = values[4, 4], values[8, 4]
|
|
50
51
|
radii, border_color, borders = values[12, 4], values[16, 4], values[20, 4]
|
|
@@ -59,12 +60,13 @@ module Zaniah
|
|
|
59
60
|
end
|
|
60
61
|
gradient = gradient_parameters(gradient)
|
|
61
62
|
if matrix == Transform.identity && borders.all?(&:zero?) && radii.all?(&:zero?) &&
|
|
62
|
-
color[3] == 1 && secondary[3] == 1 && gradient[0]
|
|
63
|
+
color[3] == 1 && secondary[3] == 1 && (1..2).cover?(gradient[0]) &&
|
|
63
64
|
[x, y, width, height].all? { |value| value == value.to_i }
|
|
64
65
|
fill_cached_gradient(bounds, x.to_i, y.to_i, width.to_i, height.to_i, color, secondary, gradient)
|
|
65
66
|
return
|
|
66
67
|
end
|
|
67
68
|
inverse = matrix.inverse
|
|
69
|
+
ramp_data = scene.quad_texture(offset)&.data
|
|
68
70
|
top, bottom = [bounds.y.floor, 0].max, [bounds.bottom.ceil, @height].min
|
|
69
71
|
left, right = [bounds.x.floor, 0].max, [bounds.right.ceil, @width].min
|
|
70
72
|
pixel_y = top
|
|
@@ -80,7 +82,14 @@ module Zaniah
|
|
|
80
82
|
distance = Math.sqrt([quad_x, 0].max**2 + [quad_y, 0].max**2) + [[quad_x, quad_y].max, 0].min - radius
|
|
81
83
|
coverage = (0.5 - distance).clamp(0, 1)
|
|
82
84
|
if coverage.positive?
|
|
83
|
-
fill = gradient[0].zero?
|
|
85
|
+
fill = if gradient[0].zero?
|
|
86
|
+
color
|
|
87
|
+
elsif ramp_data
|
|
88
|
+
ramp_index = (secondary[0].to_i * 256 + (gradient_offset(gradient, local_x, local_y, width, height) * 255).round) * 4
|
|
89
|
+
4.times.map { |index| ramp_data.getbyte(ramp_index + index) / 255.0 }.tap { |sample| sample[3] *= color[3] }
|
|
90
|
+
else
|
|
91
|
+
mix(color, secondary, gradient_offset(gradient, local_x, local_y, width, height))
|
|
92
|
+
end
|
|
84
93
|
edge = [local_y, width - local_x, height - local_y, local_x].each_with_index.min_by(&:first).last
|
|
85
94
|
border = borders[edge]
|
|
86
95
|
dashed = values[39] == 1 && ((edge.odd? ? local_y : local_x) % 6) >= 3
|
|
@@ -93,6 +102,45 @@ module Zaniah
|
|
|
93
102
|
end
|
|
94
103
|
end
|
|
95
104
|
|
|
105
|
+
def draw_shadow(values, clip)
|
|
106
|
+
x, y, width, height = values.first(4)
|
|
107
|
+
color, radii = values[4, 4], values[12, 4]
|
|
108
|
+
blur, spread, inset = values[30], values[38], values[39] == 1
|
|
109
|
+
return if inset && blur.zero? && spread.zero?
|
|
110
|
+
matrix = Transform.new(*values[32, 6])
|
|
111
|
+
inverse = matrix.inverse
|
|
112
|
+
bounds = transformed_bounds(x, y, width, height, matrix).intersect(clip)
|
|
113
|
+
margin = inset ? 0 : spread + blur * 3 + 1
|
|
114
|
+
origin_x, origin_y = x + margin, y + margin
|
|
115
|
+
content_width, content_height = width - margin * 2, height - margin * 2
|
|
116
|
+
top, bottom = [bounds.y.floor, 0].max, [bounds.bottom.ceil, @height].min
|
|
117
|
+
left, right = [bounds.x.floor, 0].max, [bounds.right.ceil, @width].min
|
|
118
|
+
(top...bottom).each do |pixel_y|
|
|
119
|
+
(left...right).each do |pixel_x|
|
|
120
|
+
point = inverse.apply(Point.new(pixel_x + 0.5, pixel_y + 0.5))
|
|
121
|
+
local_x, local_y = point.x - origin_x, point.y - origin_y
|
|
122
|
+
radius = local_y < content_height / 2.0 ? (local_x < content_width / 2.0 ? radii[0] : radii[1]) : (local_x < content_width / 2.0 ? radii[3] : radii[2])
|
|
123
|
+
radius = radius.clamp(0, [content_width, content_height].min / 2.0)
|
|
124
|
+
qx = (local_x - content_width / 2.0).abs - content_width / 2.0 + radius
|
|
125
|
+
qy = (local_y - content_height / 2.0).abs - content_height / 2.0 + radius
|
|
126
|
+
distance = Math.hypot([qx, 0].max, [qy, 0].max) + [[qx, qy].max, 0].min - radius
|
|
127
|
+
coverage = if inset
|
|
128
|
+
inside = (0.5 - distance).clamp(0, 1)
|
|
129
|
+
inside * (blur.zero? ? (distance + spread + 0.5).clamp(0, 1) : (0.5 + 0.5 * erf_approx((distance + spread) / (blur * Math.sqrt(2)))))
|
|
130
|
+
else
|
|
131
|
+
blur.zero? ? (0.5 - distance + spread).clamp(0, 1) : (0.5 - 0.5 * erf_approx((distance - spread) / (blur * Math.sqrt(2))))
|
|
132
|
+
end
|
|
133
|
+
blend(pixel_x, pixel_y, *color.first(3), color[3] * coverage) if coverage.positive?
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def erf_approx(value)
|
|
139
|
+
sign = value.negative? ? -1 : 1
|
|
140
|
+
square = value * value
|
|
141
|
+
sign * Math.sqrt(1 - Math.exp(-square * (4.0 / Math::PI + 0.147 * square) / (1 + 0.147 * square)))
|
|
142
|
+
end
|
|
143
|
+
|
|
96
144
|
def fill_rect(bounds, red, green, blue)
|
|
97
145
|
left, right = [bounds.x.to_i, 0].max, [bounds.right.to_i, @width].min
|
|
98
146
|
top, bottom = [bounds.y.to_i, 0].max, [bounds.bottom.to_i, @height].min
|
|
@@ -216,7 +264,11 @@ module Zaniah
|
|
|
216
264
|
def gradient_offset(gradient, x, y, width, height)
|
|
217
265
|
kind, first, scale, cosine, sine, center_x, center_y, radius = gradient
|
|
218
266
|
nx, ny = x / width, y / height
|
|
219
|
-
raw = kind
|
|
267
|
+
raw = case kind
|
|
268
|
+
when 1, 4 then (nx - 0.5) * cosine + (ny - 0.5) * sine + 0.5
|
|
269
|
+
when 2, 5 then Math.hypot(nx - center_x, ny - center_y) / radius
|
|
270
|
+
else ((Math.atan2(ny - center_y, nx - center_x) - Math.atan2(sine, cosine)) / (2 * Math::PI)) % 1
|
|
271
|
+
end
|
|
220
272
|
((raw - first) * scale).clamp(0, 1)
|
|
221
273
|
end
|
|
222
274
|
|
|
@@ -10,20 +10,29 @@ layout(location = 6) in vec4 widths;
|
|
|
10
10
|
layout(location = 7) in vec4 gradient;
|
|
11
11
|
layout(location = 8) in vec4 center_kind;
|
|
12
12
|
layout(location = 9) in float dash;
|
|
13
|
+
layout(location = 10) in float spread;
|
|
13
14
|
|
|
14
15
|
layout(set = 0, binding = 0) uniform sampler2D atlas;
|
|
15
16
|
layout(location = 0) out vec4 fragment_color;
|
|
16
17
|
|
|
18
|
+
float approx_erf(float value) {
|
|
19
|
+
float square = value * value;
|
|
20
|
+
return sign(value) * sqrt(1.0 - exp(-square * (1.27323954474 + 0.147 * square) / (1.0 + 0.147 * square)));
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
void main() {
|
|
18
24
|
vec4 fill = color;
|
|
19
25
|
if (center_kind.w == 0.0) {
|
|
20
26
|
if (gradient.x > 0.0) {
|
|
21
27
|
vec2 normalized = local_position / primitive_size;
|
|
22
|
-
float raw = gradient.x == 1.0
|
|
28
|
+
float raw = gradient.x == 1.0 || gradient.x == 4.0
|
|
23
29
|
? dot(normalized - 0.5, vec2(cos(radians(gradient.w)), sin(radians(gradient.w)))) + 0.5
|
|
24
|
-
: length(normalized - center_kind.xy) / center_kind.z
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
: gradient.x == 2.0 || gradient.x == 5.0 ? length(normalized - center_kind.xy) / center_kind.z
|
|
31
|
+
: fract((atan(normalized.y - center_kind.y, normalized.x - center_kind.x) - radians(gradient.w)) / 6.28318530718 + 1.0);
|
|
32
|
+
float amount = clamp((raw - gradient.y) / max(gradient.z - gradient.y, 0.000001), 0.0, 1.0);
|
|
33
|
+
vec2 ramp_uv = vec2((floor(amount * 255.0 + 0.5) + 0.5) / 256.0, (secondary.x + 0.5) / 256.0);
|
|
34
|
+
fill = gradient.x >= 4.0 ? texture(atlas, ramp_uv) * vec4(1.0,1.0,1.0,color.a)
|
|
35
|
+
: mix(color, secondary, amount);
|
|
27
36
|
}
|
|
28
37
|
float radius = local_position.y < primitive_size.y / 2.0
|
|
29
38
|
? (local_position.x < primitive_size.x / 2.0 ? radii.x : radii.y)
|
|
@@ -41,6 +50,19 @@ void main() {
|
|
|
41
50
|
if (border_width > 0.0 && distance >= -border_width &&
|
|
42
51
|
!(dash == 1.0 && mod(coordinate, 6.0) >= 3.0)) fill = border;
|
|
43
52
|
fill.a *= coverage;
|
|
53
|
+
} else if (center_kind.w == 4.0) {
|
|
54
|
+
float margin = dash == 1.0 ? 0.0 : spread + center_kind.z * 3.0 + 1.0;
|
|
55
|
+
vec2 original_size = primitive_size - 2.0 * margin;
|
|
56
|
+
vec2 p = local_position - margin;
|
|
57
|
+
float radius = p.y < original_size.y/2.0 ? (p.x < original_size.x/2.0 ? radii.x : radii.y) : (p.x < original_size.x/2.0 ? radii.w : radii.z);
|
|
58
|
+
radius = clamp(radius, 0.0, min(original_size.x,original_size.y)/2.0);
|
|
59
|
+
vec2 q = abs(p - original_size/2.0) - original_size/2.0 + radius;
|
|
60
|
+
float distance = length(max(q,0.0)) + min(max(q.x,q.y),0.0) - radius;
|
|
61
|
+
float sigma = center_kind.z;
|
|
62
|
+
float coverage = dash == 1.0
|
|
63
|
+
? clamp(0.5 - distance, 0.0, 1.0) * (sigma == 0.0 ? clamp(distance + spread + 0.5, 0.0, 1.0) : 0.5 + 0.5 * approx_erf((distance + spread) / (sigma * 1.41421356237)))
|
|
64
|
+
: sigma == 0.0 ? clamp(0.5 - distance + spread, 0.0, 1.0) : 0.5 - 0.5 * approx_erf((distance - spread) / (sigma * 1.41421356237));
|
|
65
|
+
fill.a *= coverage;
|
|
44
66
|
} else if (center_kind.w == 1.0 || center_kind.w == 2.0) {
|
|
45
67
|
vec2 uv = widths.xy + local_position / primitive_size * widths.zw;
|
|
46
68
|
vec4 sample_color = texture(atlas, uv);
|
|
Binary file
|
|
@@ -23,6 +23,7 @@ layout(location = 6) out vec4 widths;
|
|
|
23
23
|
layout(location = 7) out vec4 gradient;
|
|
24
24
|
layout(location = 8) out vec4 center_kind;
|
|
25
25
|
layout(location = 9) out float dash;
|
|
26
|
+
layout(location = 10) out float spread;
|
|
26
27
|
|
|
27
28
|
void main() {
|
|
28
29
|
vec2 corners[4] = vec2[4](vec2(0, 0), vec2(1, 0), vec2(0, 1), vec2(1, 1));
|
|
@@ -47,4 +48,5 @@ void main() {
|
|
|
47
48
|
gradient = instance_gradient;
|
|
48
49
|
center_kind = instance_center_kind;
|
|
49
50
|
dash = instance_translation.w;
|
|
51
|
+
spread = instance_translation.z;
|
|
50
52
|
}
|
|
Binary file
|
data/lib/zaniah/image.rb
CHANGED
|
@@ -25,14 +25,14 @@ module Zaniah
|
|
|
25
25
|
@texture = GPU::Texture.new(width, height, data: pixels)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
attr_reader :frames
|
|
28
|
+
attr_reader :frames, :texture
|
|
29
29
|
|
|
30
30
|
def request_layout(_cx)
|
|
31
31
|
@layout_node = Layout::Node.new(style: @style, measure: ->(_width, _height) { [@texture.width, @texture.height] })
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def paint(bounds, _state, _prepaint, cx)
|
|
35
|
-
cx.scene.
|
|
35
|
+
cx.scene.image(bounds.x, bounds.y, bounds.width, bounds.height, image: self, texture: @texture)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
private
|
|
@@ -291,6 +291,7 @@ module Zaniah
|
|
|
291
291
|
frame_started = MONOTONIC_CLOCK.call
|
|
292
292
|
@text_system.scale_factor = @scale_factor if @text_system.respond_to?(:scale_factor=)
|
|
293
293
|
@scene.clear
|
|
294
|
+
@scene.vector_sink.size = @content_size if @scene.vector_sink&.respond_to?(:size=)
|
|
294
295
|
@text_runs.clear
|
|
295
296
|
@text_system.start_frame if @text_system.respond_to?(:start_frame)
|
|
296
297
|
@used_state.clear
|