zaniah 0.2.0 → 0.3.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.
Files changed (205) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/LICENSE.txt +18 -0
  4. data/README.md +14 -4
  5. data/assets/icons/check.svg +1 -0
  6. data/assets/icons/close.svg +1 -0
  7. data/assets/icons/info.svg +1 -0
  8. data/assets/icons/menu.svg +1 -0
  9. data/assets/icons/search.svg +1 -0
  10. data/assets/icons/warning.svg +1 -0
  11. data/docs/accessibility.md +61 -0
  12. data/docs/adr/004-style-resolution.md +17 -0
  13. data/docs/adr/005-instance-layout.md +43 -0
  14. data/docs/adr/006-component-model.md +17 -0
  15. data/docs/adr/007-text-buffer.md +17 -0
  16. data/docs/adr/008-accessibility-bridge.md +19 -0
  17. data/docs/adr/009-grid-scope.md +17 -0
  18. data/docs/animation.md +46 -0
  19. data/docs/components.md +109 -0
  20. data/docs/gallery/command-palette-dark.png +0 -0
  21. data/docs/gallery/command-palette-high_contrast.png +0 -0
  22. data/docs/gallery/command-palette-light.png +0 -0
  23. data/docs/gallery/components-dark.png +0 -0
  24. data/docs/gallery/components-high_contrast.png +0 -0
  25. data/docs/gallery/components-light.png +0 -0
  26. data/docs/gallery/context-menu-dark.png +0 -0
  27. data/docs/gallery/context-menu-high_contrast.png +0 -0
  28. data/docs/gallery/context-menu-light.png +0 -0
  29. data/docs/gallery/dialog-dark.png +0 -0
  30. data/docs/gallery/dialog-high_contrast.png +0 -0
  31. data/docs/gallery/dialog-light.png +0 -0
  32. data/docs/gallery/drawer-left-dark.png +0 -0
  33. data/docs/gallery/drawer-left-high_contrast.png +0 -0
  34. data/docs/gallery/drawer-left-light.png +0 -0
  35. data/docs/gallery/drawer-right-dark.png +0 -0
  36. data/docs/gallery/drawer-right-high_contrast.png +0 -0
  37. data/docs/gallery/drawer-right-light.png +0 -0
  38. data/docs/gallery/menu-dark.png +0 -0
  39. data/docs/gallery/menu-high_contrast.png +0 -0
  40. data/docs/gallery/menu-light.png +0 -0
  41. data/docs/gallery/modal-dark.png +0 -0
  42. data/docs/gallery/modal-high_contrast.png +0 -0
  43. data/docs/gallery/modal-light.png +0 -0
  44. data/docs/gallery/popover-bottom-dark.png +0 -0
  45. data/docs/gallery/popover-bottom-high_contrast.png +0 -0
  46. data/docs/gallery/popover-bottom-light.png +0 -0
  47. data/docs/gallery/popover-left-dark.png +0 -0
  48. data/docs/gallery/popover-left-high_contrast.png +0 -0
  49. data/docs/gallery/popover-left-light.png +0 -0
  50. data/docs/gallery/popover-right-dark.png +0 -0
  51. data/docs/gallery/popover-right-high_contrast.png +0 -0
  52. data/docs/gallery/popover-right-light.png +0 -0
  53. data/docs/gallery/popover-top-dark.png +0 -0
  54. data/docs/gallery/popover-top-high_contrast.png +0 -0
  55. data/docs/gallery/popover-top-light.png +0 -0
  56. data/docs/gallery/toast-danger-dark.png +0 -0
  57. data/docs/gallery/toast-danger-high_contrast.png +0 -0
  58. data/docs/gallery/toast-danger-light.png +0 -0
  59. data/docs/gallery/toast-info-dark.png +0 -0
  60. data/docs/gallery/toast-info-high_contrast.png +0 -0
  61. data/docs/gallery/toast-info-light.png +0 -0
  62. data/docs/gallery/toast-success-dark.png +0 -0
  63. data/docs/gallery/toast-success-high_contrast.png +0 -0
  64. data/docs/gallery/toast-success-light.png +0 -0
  65. data/docs/gallery/toast-warning-dark.png +0 -0
  66. data/docs/gallery/toast-warning-high_contrast.png +0 -0
  67. data/docs/gallery/toast-warning-light.png +0 -0
  68. data/docs/gallery/tooltip-bottom-dark.png +0 -0
  69. data/docs/gallery/tooltip-bottom-high_contrast.png +0 -0
  70. data/docs/gallery/tooltip-bottom-light.png +0 -0
  71. data/docs/gallery/tooltip-left-dark.png +0 -0
  72. data/docs/gallery/tooltip-left-high_contrast.png +0 -0
  73. data/docs/gallery/tooltip-left-light.png +0 -0
  74. data/docs/gallery/tooltip-right-dark.png +0 -0
  75. data/docs/gallery/tooltip-right-high_contrast.png +0 -0
  76. data/docs/gallery/tooltip-right-light.png +0 -0
  77. data/docs/gallery/tooltip-top-dark.png +0 -0
  78. data/docs/gallery/tooltip-top-high_contrast.png +0 -0
  79. data/docs/gallery/tooltip-top-light.png +0 -0
  80. data/docs/index.html +74 -0
  81. data/docs/layout.md +31 -0
  82. data/docs/native.md +7 -2
  83. data/docs/site.css +32 -0
  84. data/docs/text.md +35 -0
  85. data/docs/theme.md +50 -0
  86. data/docs/tui.md +36 -0
  87. data/examples/gallery.rb +155 -0
  88. data/examples/native_ime.rb +13 -6
  89. data/lib/zaniah/accessibility/linux/service.rb +361 -0
  90. data/lib/zaniah/accessibility/linux.rb +23 -0
  91. data/lib/zaniah/accessibility/mac.rb +136 -0
  92. data/lib/zaniah/accessibility/native_tree.rb +53 -0
  93. data/lib/zaniah/accessibility/node.rb +12 -0
  94. data/lib/zaniah/accessibility/tree.rb +61 -0
  95. data/lib/zaniah/accessibility/windows/provider.rb +273 -0
  96. data/lib/zaniah/accessibility/windows.rb +38 -0
  97. data/lib/zaniah/accessibility.rb +63 -0
  98. data/lib/zaniah/animation/animator.rb +95 -0
  99. data/lib/zaniah/animation/easing.rb +45 -0
  100. data/lib/zaniah/animation/spring.rb +36 -0
  101. data/lib/zaniah/animation/transition.rb +5 -0
  102. data/lib/zaniah/animation.rb +6 -0
  103. data/lib/zaniah/app.rb +22 -2
  104. data/lib/zaniah/color.rb +16 -0
  105. data/lib/zaniah/devtools/hot_reload.rb +40 -0
  106. data/lib/zaniah/devtools/inspector.rb +111 -0
  107. data/lib/zaniah/devtools/stats_overlay.rb +16 -0
  108. data/lib/zaniah/devtools.rb +13 -0
  109. data/lib/zaniah/element.rb +210 -15
  110. data/lib/zaniah/frame_context.rb +4 -1
  111. data/lib/zaniah/gpu/frame_encoder.rb +10 -1
  112. data/lib/zaniah/gpu/instance_packing.rb +33 -5
  113. data/lib/zaniah/gpu/metal.rb +26 -10
  114. data/lib/zaniah/gpu/open_gl.rb +35 -14
  115. data/lib/zaniah/gpu/software.rb +116 -14
  116. data/lib/zaniah/gpu/vulkan/scene.frag +51 -0
  117. data/lib/zaniah/gpu/vulkan/scene.frag.spv +0 -0
  118. data/lib/zaniah/gpu/vulkan/scene.vert +50 -0
  119. data/lib/zaniah/gpu/vulkan/scene.vert.spv +0 -0
  120. data/lib/zaniah/gpu/vulkan.rb +282 -36
  121. data/lib/zaniah/gpu.rb +8 -3
  122. data/lib/zaniah/input/dispatcher.rb +96 -12
  123. data/lib/zaniah/input/focus_handle.rb +13 -4
  124. data/lib/zaniah/input/focus_tree.rb +77 -0
  125. data/lib/zaniah/input/keymap.rb +77 -0
  126. data/lib/zaniah/input.rb +1 -0
  127. data/lib/zaniah/interactivity.rb +30 -0
  128. data/lib/zaniah/layout/engine.rb +173 -36
  129. data/lib/zaniah/layout/grid.rb +134 -0
  130. data/lib/zaniah/layout/style.rb +16 -1
  131. data/lib/zaniah/layout.rb +1 -0
  132. data/lib/zaniah/length.rb +3 -0
  133. data/lib/zaniah/length_units.rb +7 -0
  134. data/lib/zaniah/list.rb +27 -15
  135. data/lib/zaniah/platform/appearance.rb +11 -0
  136. data/lib/zaniah/platform/headless/window.rb +101 -9
  137. data/lib/zaniah/platform/linux/appearance_aware.rb +8 -0
  138. data/lib/zaniah/platform/linux/wayland_window.rb +3 -1
  139. data/lib/zaniah/platform/linux/window.rb +4 -2
  140. data/lib/zaniah/platform/mac/app.rb +1 -1
  141. data/lib/zaniah/platform/mac/core_text.rb +3 -0
  142. data/lib/zaniah/platform/mac/window.rb +9 -1
  143. data/lib/zaniah/platform/tui/window.rb +1 -1
  144. data/lib/zaniah/platform/windows/window.rb +12 -2
  145. data/lib/zaniah/platform.rb +1 -0
  146. data/lib/zaniah/scene.rb +137 -28
  147. data/lib/zaniah/scroll_state.rb +117 -0
  148. data/lib/zaniah/scroll_view.rb +94 -0
  149. data/lib/zaniah/style/gradient.rb +28 -0
  150. data/lib/zaniah/style/ring.rb +13 -0
  151. data/lib/zaniah/style/shadow.rb +13 -0
  152. data/lib/zaniah/style/style_set.rb +43 -0
  153. data/lib/zaniah/style/transform.rb +40 -0
  154. data/lib/zaniah/svg.rb +19 -0
  155. data/lib/zaniah/text.rb +261 -6
  156. data/lib/zaniah/text_buffer.rb +119 -0
  157. data/lib/zaniah/text_selection.rb +18 -0
  158. data/lib/zaniah/text_system/atlas_cache.rb +1 -1
  159. data/lib/zaniah/text_system/fallback.rb +24 -0
  160. data/lib/zaniah/text_system/font_db.rb +4 -1
  161. data/lib/zaniah/text_system/kinsoku.rb +28 -0
  162. data/lib/zaniah/text_system/line_breaker.rb +54 -0
  163. data/lib/zaniah/text_system/paragraph.rb +99 -0
  164. data/lib/zaniah/text_system/renderer.rb +13 -2
  165. data/lib/zaniah/text_system/typesetter.rb +4 -0
  166. data/lib/zaniah/text_system.rb +3 -0
  167. data/lib/zaniah/theme.rb +39 -0
  168. data/lib/zaniah/ui/charts.rb +165 -0
  169. data/lib/zaniah/ui/choice_fields.rb +134 -0
  170. data/lib/zaniah/ui/component.rb +73 -0
  171. data/lib/zaniah/ui/controls.rb +439 -0
  172. data/lib/zaniah/ui/data.rb +364 -0
  173. data/lib/zaniah/ui/editors.rb +67 -0
  174. data/lib/zaniah/ui/forms.rb +131 -0
  175. data/lib/zaniah/ui/layout_components.rb +205 -0
  176. data/lib/zaniah/ui/modal.rb +129 -0
  177. data/lib/zaniah/ui/overlays.rb +282 -0
  178. data/lib/zaniah/ui/pickers.rb +169 -0
  179. data/lib/zaniah/ui/primitives.rb +199 -0
  180. data/lib/zaniah/ui/scrollbar.rb +104 -0
  181. data/lib/zaniah/ui/structure.rb +224 -0
  182. data/lib/zaniah/ui/text_inputs.rb +169 -0
  183. data/lib/zaniah/ui/variants.rb +23 -0
  184. data/lib/zaniah/ui.rb +18 -0
  185. data/lib/zaniah/unicode.rb +42 -0
  186. data/lib/zaniah/uniform_list.rb +27 -6
  187. data/lib/zaniah/version.rb +1 -1
  188. data/lib/zaniah.rb +14 -0
  189. data/sig/accessibility.rbs +34 -0
  190. data/sig/animation.rbs +54 -0
  191. data/sig/app.rbs +2 -1
  192. data/sig/devtools.rbs +37 -0
  193. data/sig/elements.rbs +88 -6
  194. data/sig/gpu.rbs +9 -2
  195. data/sig/input.rbs +31 -5
  196. data/sig/layout.rbs +2 -1
  197. data/sig/platform.rbs +24 -4
  198. data/sig/scene.rbs +15 -2
  199. data/sig/style.rbs +82 -0
  200. data/sig/text.rbs +35 -0
  201. data/sig/theme.rbs +64 -0
  202. data/sig/ui.rbs +390 -0
  203. data/sig/zaniah.rbs +56 -3
  204. data/tools/generate_component_gallery.rb +48 -0
  205. metadata +147 -2
@@ -12,42 +12,63 @@ module Zaniah
12
12
  #version 330 core
13
13
  layout(location=0) in vec4 rect;
14
14
  layout(location=1) in vec4 tint;
15
- layout(location=2) in vec4 corners;
16
- layout(location=3) in vec4 border_color;
17
- layout(location=4) in vec4 extra;
18
- layout(location=5) in vec4 source;
15
+ layout(location=2) in vec4 tint2;
16
+ layout(location=3) in vec4 corners;
17
+ layout(location=4) in vec4 border_color;
18
+ layout(location=5) in vec4 edges_source;
19
+ layout(location=6) in vec4 gradient_data;
20
+ layout(location=7) in vec4 gradient_center_kind;
21
+ layout(location=8) in vec4 matrix;
22
+ layout(location=9) in vec4 translation_flags;
19
23
  uniform vec2 viewport;
20
24
  out vec2 local_position; out vec2 size; out vec4 color; out vec4 radii;
21
- out vec4 border; out vec4 info; out vec2 uv;
25
+ out vec4 secondary; out vec4 border; out vec4 widths; out vec4 gradient;
26
+ out vec4 center_kind; out vec2 uv; out float dash;
22
27
  void main() {
23
28
  vec2 vertices[4] = vec2[4](vec2(0,0),vec2(1,0),vec2(0,1),vec2(1,1));
24
29
  vec2 corner = vertices[gl_VertexID];
25
30
  vec2 position = rect.xy + corner * rect.zw;
26
- if (extra.y == 3) position = gl_VertexID == 0 ? rect.xy : gl_VertexID == 1 ? rect.zw : corners.xy;
31
+ if (gradient_center_kind.w == 3) position = gl_VertexID == 0 ? rect.xy : gl_VertexID == 1 ? rect.zw : corners.xy;
32
+ position = vec2(matrix.x * position.x + matrix.z * position.y + translation_flags.x,
33
+ matrix.y * position.x + matrix.w * position.y + translation_flags.y);
27
34
  gl_Position = vec4(position.x / viewport.x * 2 - 1, 1 - position.y / viewport.y * 2, 0, 1);
28
35
  local_position = corner * rect.zw; size = rect.zw; color = tint;
29
- radii = corners; border = border_color; info = extra; uv = source.xy + corner * source.zw;
36
+ secondary = tint2; radii = corners; border = border_color; widths = edges_source;
37
+ gradient = gradient_data; center_kind = gradient_center_kind;
38
+ uv = edges_source.xy + corner * edges_source.zw; dash = translation_flags.w;
30
39
  }
31
40
  GLSL
32
41
  FRAGMENT = <<~GLSL.freeze
33
42
  #version 330 core
34
43
  in vec2 local_position; in vec2 size; in vec4 color; in vec4 radii;
35
- in vec4 border; in vec4 info; in vec2 uv;
44
+ in vec4 secondary; in vec4 border; in vec4 widths; in vec4 gradient;
45
+ in vec4 center_kind; in vec2 uv; in float dash;
36
46
  uniform sampler2D atlas;
37
47
  out vec4 output_color;
38
48
  void main() {
39
49
  vec4 result = color;
40
- if (info.y == 0) {
50
+ if (center_kind.w == 0) {
51
+ if (gradient.x > 0) {
52
+ vec2 normalized = local_position / size;
53
+ float raw = gradient.x == 1
54
+ ? 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
+ result = mix(color, secondary, clamp((raw - gradient.y) / max(gradient.z - gradient.y, 0.000001), 0, 1));
57
+ }
41
58
  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);
42
59
  radius = clamp(radius, 0, min(size.x,size.y)/2);
43
60
  vec2 q = abs(local_position - size/2) - size/2 + radius;
44
61
  float distance = length(max(q,0)) + min(max(q.x,q.y),0) - radius;
45
62
  float coverage = clamp(0.5 - distance, 0, 1);
46
- if (info.x > 0 && distance >= -info.x) result = border;
63
+ vec4 edge_distance = vec4(local_position.y, size.x-local_position.x, size.y-local_position.y, local_position.x);
64
+ float edge = min(min(edge_distance.x,edge_distance.y),min(edge_distance.z,edge_distance.w));
65
+ float border_width = edge == edge_distance.x ? widths.x : edge == edge_distance.y ? widths.y : edge == edge_distance.z ? widths.z : widths.w;
66
+ float coordinate = edge == edge_distance.x || edge == edge_distance.z ? local_position.x : local_position.y;
67
+ if (border_width > 0 && distance >= -border_width && !(dash == 1 && mod(coordinate,6) >= 3)) result = border;
47
68
  result.a *= coverage;
48
- } else if (info.y == 1 || info.y == 2) {
69
+ } else if (center_kind.w == 1 || center_kind.w == 2) {
49
70
  vec4 sample_color = texture(atlas,uv);
50
- if (info.y == 1) result.a *= sample_color.r; else result *= sample_color;
71
+ if (center_kind.w == 1) result.a *= sample_color.r; else result *= sample_color;
51
72
  }
52
73
  output_color = vec4(result.rgb * result.a, result.a);
53
74
  }
@@ -62,7 +83,7 @@ module Zaniah
62
83
  @vao, @buffer = generate(:glGenVertexArrays), generate(:glGenBuffers)
63
84
  gl(:glBindVertexArray, [I], V, @vao)
64
85
  gl(:glBindBuffer, [I, I], V, 0x8892, @buffer)
65
- 6.times do |location|
86
+ 10.times do |location|
66
87
  gl(:glEnableVertexAttribArray, [I], V, location)
67
88
  gl(:glVertexAttribDivisor, [I, I], V, location, 1)
68
89
  end
@@ -148,7 +169,7 @@ module Zaniah
148
169
  clip = clip ? Bounds.new(0, 0, @width, @height).intersect(clip) : Bounds.new(0, 0, @width, @height)
149
170
  next unless clip.width.positive? && clip.height.positive?
150
171
  gl(:glScissor, [I] * 4, V, (clip.x * @scale).to_i, ((@height - clip.bottom) * @scale).to_i, (clip.width * @scale).to_i, (clip.height * @scale).to_i)
151
- 6.times do |location|
172
+ 10.times do |location|
152
173
  gl(:glVertexAttribPointer, [I, I, I, I, I, P], V, location, 4, 0x1406, 0, InstancePacking::STRIDE * 4, (first * InstancePacking::STRIDE + location * 4) * 4)
153
174
  end
154
175
  native_texture(texture || @white)
@@ -14,6 +14,7 @@ module Zaniah
14
14
  @width, @height = width.to_i, height.to_i
15
15
  @viewport = Bounds.new(0, 0, @width, @height)
16
16
  @pixels = "\0".b * (@width * @height * 4)
17
+ @gradient_cache ||= {}
17
18
  end
18
19
 
19
20
  def create_texture(width, height, **options) = Texture.new(width, height, **options)
@@ -43,25 +44,48 @@ module Zaniah
43
44
  private
44
45
 
45
46
  def draw_quad(data, offset, clip)
46
- x, y, width, height, red, green, blue, alpha, top_left, top_right, bottom_right, bottom_left, border, border_red, border_green, border_blue, border_alpha = data.slice(offset, Scene::QUAD_STRIDE)
47
- bounds = Bounds.new(x, y, width, height).intersect(clip)
48
- colors = [[red, green, blue, alpha], [border_red, border_green, border_blue, border_alpha]]
47
+ values = data.slice(offset, Scene::QUAD_STRIDE)
48
+ x, y, width, height = values.first(4)
49
+ color, secondary = values[4, 4], values[8, 4]
50
+ radii, border_color, borders = values[12, 4], values[16, 4], values[20, 4]
51
+ gradient = values[24, 7]
52
+ matrix = Transform.new(*values[32, 6])
53
+ bounds = transformed_bounds(x, y, width, height, matrix).intersect(clip)
54
+ if matrix.b.zero? && matrix.c.zero? && borders.all?(&:zero?) && color[3] == 1 &&
55
+ radii.all?(&:zero?) && gradient[0].zero? &&
56
+ [bounds.x, bounds.y, bounds.width, bounds.height].all? { |value| value == value.to_i }
57
+ fill_rect(bounds, *color.first(3))
58
+ return
59
+ end
60
+ gradient = gradient_parameters(gradient)
61
+ if matrix == Transform.identity && borders.all?(&:zero?) && radii.all?(&:zero?) &&
62
+ color[3] == 1 && secondary[3] == 1 && gradient[0].positive? &&
63
+ [x, y, width, height].all? { |value| value == value.to_i }
64
+ fill_cached_gradient(bounds, x.to_i, y.to_i, width.to_i, height.to_i, color, secondary, gradient)
65
+ return
66
+ end
67
+ inverse = matrix.inverse
49
68
  top, bottom = [bounds.y.floor, 0].max, [bounds.bottom.ceil, @height].min
50
69
  left, right = [bounds.x.floor, 0].max, [bounds.right.ceil, @width].min
51
70
  pixel_y = top
52
71
  while pixel_y < bottom
53
72
  pixel_x = left
54
73
  while pixel_x < right
55
- center_x, center_y = pixel_x + 0.5, pixel_y + 0.5
56
- radius = center_y < y + height / 2.0 ? (center_x < x + width / 2.0 ? top_left : top_right) : (center_x < x + width / 2.0 ? bottom_left : bottom_right)
74
+ point = inverse.apply(Point.new(pixel_x + 0.5, pixel_y + 0.5))
75
+ local_x, local_y = point.x - x, point.y - y
76
+ radius = local_y < height / 2.0 ? (local_x < width / 2.0 ? radii[0] : radii[1]) : (local_x < width / 2.0 ? radii[3] : radii[2])
57
77
  radius = radius.clamp(0, [width, height].min / 2.0)
58
- quad_x = (center_x - x - width / 2.0).abs - width / 2.0 + radius
59
- quad_y = (center_y - y - height / 2.0).abs - height / 2.0 + radius
78
+ quad_x = (local_x - width / 2.0).abs - width / 2.0 + radius
79
+ quad_y = (local_y - height / 2.0).abs - height / 2.0 + radius
60
80
  distance = Math.sqrt([quad_x, 0].max**2 + [quad_y, 0].max**2) + [[quad_x, quad_y].max, 0].min - radius
61
81
  coverage = (0.5 - distance).clamp(0, 1)
62
82
  if coverage.positive?
63
- color = colors[border.positive? && distance >= -border ? 1 : 0]
64
- blend(pixel_x, pixel_y, color[0], color[1], color[2], color[3] * coverage)
83
+ fill = gradient[0].zero? ? color : mix(color, secondary, gradient_offset(gradient, local_x, local_y, width, height))
84
+ edge = [local_y, width - local_x, height - local_y, local_x].each_with_index.min_by(&:first).last
85
+ border = borders[edge]
86
+ dashed = values[39] == 1 && ((edge.odd? ? local_y : local_x) % 6) >= 3
87
+ fill = border_color if border.positive? && distance >= -border && !dashed
88
+ blend(pixel_x, pixel_y, fill[0], fill[1], fill[2], fill[3] * coverage)
65
89
  end
66
90
  pixel_x += 1
67
91
  end
@@ -69,15 +93,62 @@ module Zaniah
69
93
  end
70
94
  end
71
95
 
96
+ def fill_rect(bounds, red, green, blue)
97
+ left, right = [bounds.x.to_i, 0].max, [bounds.right.to_i, @width].min
98
+ top, bottom = [bounds.y.to_i, 0].max, [bounds.bottom.to_i, @height].min
99
+ return if left >= right || top >= bottom
100
+ row = [red, green, blue, 1].map { |value| (value.clamp(0, 1) * 255).round }.pack("C4") * (right - left)
101
+ (top...bottom).each { |pixel_y| @pixels[pixel_y * @width * 4 + left * 4, row.bytesize] = row }
102
+ end
103
+
104
+ def fill_cached_gradient(bounds, x, y, width, height, color, secondary, gradient)
105
+ return if width <= 0 || height <= 0
106
+ @gradient_cache.clear if @gradient_cache.length >= 256
107
+ key = [width, height, color, secondary, gradient]
108
+ tile = @gradient_cache[key] ||= gradient_tile(width, height, color, secondary, gradient)
109
+ left, right = [bounds.x.ceil, 0].max, [bounds.right.floor, @width].min
110
+ top, bottom = [bounds.y.ceil, 0].max, [bounds.bottom.floor, @height].min
111
+ return if left >= right || top >= bottom
112
+ bytes = (right - left) * 4
113
+ (top...bottom).each do |pixel_y|
114
+ source = ((pixel_y - y) * width + left - x) * 4
115
+ @pixels[pixel_y * @width * 4 + left * 4, bytes] = tile.byteslice(source, bytes)
116
+ end
117
+ end
118
+
119
+ def gradient_tile(width, height, left, right, gradient)
120
+ kind, first, scale, cosine, sine, center_x, center_y, radius = gradient
121
+ starts = left.first(3).map { |value| value * 255 }
122
+ deltas = 3.times.map { |index| (right[index] - left[index]) * 255 }
123
+ bytes = String.new(capacity: width * height * 4, encoding: Encoding::BINARY)
124
+ height.times do |pixel_y|
125
+ ny = (pixel_y + 0.5) / height
126
+ width.times do |pixel_x|
127
+ nx = (pixel_x + 0.5) / width
128
+ raw = kind == 1 ? (nx - 0.5) * cosine + (ny - 0.5) * sine + 0.5 : Math.hypot(nx - center_x, ny - center_y) / radius
129
+ amount = ((raw - first) * scale).clamp(0, 1)
130
+ bytes << (starts[0] + deltas[0] * amount).round.clamp(0, 255)
131
+ bytes << (starts[1] + deltas[1] * amount).round.clamp(0, 255)
132
+ bytes << (starts[2] + deltas[2] * amount).round.clamp(0, 255)
133
+ bytes << 255
134
+ end
135
+ end
136
+ bytes.freeze
137
+ end
138
+
72
139
  def draw_sprite(scene, offset, clip)
73
- x, y, width, height, red, green, blue, alpha, id, source_x, source_y, source_width, source_height = scene.sprite_data.slice(offset, 13)
140
+ x, y, width, height, red, green, blue, alpha, id, source_x, source_y, source_width, source_height = scene.sprite_data.slice(offset, Scene::SPRITE_STRIDE)
74
141
  return if width <= 0 || height <= 0
75
142
  texture = scene.textures.fetch(id)
76
- bounds = Bounds.new(x, y, width, height).intersect(clip)
143
+ matrix = scene.sprite_transform(offset)
144
+ inverse = matrix.inverse
145
+ bounds = transformed_bounds(x, y, width, height, matrix).intersect(clip)
77
146
  ([bounds.y.ceil, 0].max...[bounds.bottom.ceil, @height].min).each do |pixel_y|
78
147
  ([bounds.x.ceil, 0].max...[bounds.right.ceil, @width].min).each do |pixel_x|
79
- texture_x = (source_x + (pixel_x + 0.5 - x) * source_width / width).floor.clamp(0, texture.width - 1)
80
- texture_y = (source_y + (pixel_y + 0.5 - y) * source_height / height).floor.clamp(0, texture.height - 1)
148
+ point = inverse.apply(Point.new(pixel_x + 0.5, pixel_y + 0.5))
149
+ next unless point.x >= x && point.y >= y && point.x < x + width && point.y < y + height
150
+ texture_x = (source_x + (point.x - x) * source_width / width).floor.clamp(0, texture.width - 1)
151
+ texture_y = (source_y + (point.y - y) * source_height / height).floor.clamp(0, texture.height - 1)
81
152
  index = texture_y * texture.width + texture_x
82
153
  if texture.format == :r8
83
154
  blend(pixel_x, pixel_y, red, green, blue, alpha * texture.data.getbyte(index) / 255.0)
@@ -93,7 +164,12 @@ module Zaniah
93
164
  end
94
165
 
95
166
  def draw_triangle(data, offset, clip)
96
- x0, y0, x1, y1, x2, y2, red, green, blue, alpha = data.slice(offset, 10)
167
+ x0, y0, x1, y1, x2, y2, red, green, blue, alpha, *transform = data.slice(offset, 16)
168
+ matrix = Transform.new(*transform)
169
+ x0, y0, x1, y1, x2, y2 = [[x0, y0], [x1, y1], [x2, y2]].flat_map do |x, y|
170
+ point = matrix.apply(Point.new(x, y))
171
+ [point.x, point.y]
172
+ end
97
173
  area = (x1 - x0) * (y2 - y0) - (y1 - y0) * (x2 - x0)
98
174
  return if area.zero?
99
175
  bounds = Bounds.new([x0, x1, x2].min, [y0, y1, y2].min,
@@ -121,6 +197,32 @@ module Zaniah
121
197
  end
122
198
  @pixels.setbyte(index + 3, (combined_alpha * 255).round)
123
199
  end
200
+
201
+ def transformed_bounds(x, y, width, height, matrix)
202
+ points = [[x, y], [x + width, y], [x, y + height], [x + width, y + height]].map do |px, py|
203
+ matrix.apply(Point.new(px, py))
204
+ end
205
+ left, right = points.map(&:x).minmax
206
+ top, bottom = points.map(&:y).minmax
207
+ Bounds.new(left, top, right - left, bottom - top)
208
+ end
209
+
210
+ def gradient_parameters(values)
211
+ kind, first, last, angle, center_x, center_y, radius = values
212
+ radians = angle * Math::PI / 180
213
+ [kind, first, 1.0 / [last - first, Float::EPSILON].max, Math.cos(radians), Math.sin(radians), center_x, center_y, radius]
214
+ end
215
+
216
+ def gradient_offset(gradient, x, y, width, height)
217
+ kind, first, scale, cosine, sine, center_x, center_y, radius = gradient
218
+ nx, ny = x / width, y / height
219
+ raw = kind == 1 ? (nx - 0.5) * cosine + (ny - 0.5) * sine + 0.5 : Math.hypot(nx - center_x, ny - center_y) / radius
220
+ ((raw - first) * scale).clamp(0, 1)
221
+ end
222
+
223
+ def mix(left, right, amount)
224
+ 4.times.map { |index| left[index] + (right[index] - left[index]) * amount }
225
+ end
124
226
  end
125
227
  end
126
228
  end
@@ -0,0 +1,51 @@
1
+ #version 450
2
+
3
+ layout(location = 0) in vec2 local_position;
4
+ layout(location = 1) in vec2 primitive_size;
5
+ layout(location = 2) in vec4 color;
6
+ layout(location = 3) in vec4 secondary;
7
+ layout(location = 4) in vec4 radii;
8
+ layout(location = 5) in vec4 border;
9
+ layout(location = 6) in vec4 widths;
10
+ layout(location = 7) in vec4 gradient;
11
+ layout(location = 8) in vec4 center_kind;
12
+ layout(location = 9) in float dash;
13
+
14
+ layout(set = 0, binding = 0) uniform sampler2D atlas;
15
+ layout(location = 0) out vec4 fragment_color;
16
+
17
+ void main() {
18
+ vec4 fill = color;
19
+ if (center_kind.w == 0.0) {
20
+ if (gradient.x > 0.0) {
21
+ vec2 normalized = local_position / primitive_size;
22
+ float raw = gradient.x == 1.0
23
+ ? 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
+ fill = mix(color, secondary,
26
+ clamp((raw - gradient.y) / max(gradient.z - gradient.y, 0.000001), 0.0, 1.0));
27
+ }
28
+ float radius = local_position.y < primitive_size.y / 2.0
29
+ ? (local_position.x < primitive_size.x / 2.0 ? radii.x : radii.y)
30
+ : (local_position.x < primitive_size.x / 2.0 ? radii.w : radii.z);
31
+ radius = clamp(radius, 0.0, min(primitive_size.x, primitive_size.y) / 2.0);
32
+ vec2 q = abs(local_position - primitive_size / 2.0) - primitive_size / 2.0 + radius;
33
+ float distance = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius;
34
+ float coverage = clamp(0.5 - distance, 0.0, 1.0);
35
+ vec4 edge_distance = vec4(local_position.y, primitive_size.x - local_position.x,
36
+ primitive_size.y - local_position.y, local_position.x);
37
+ float edge = min(min(edge_distance.x, edge_distance.y), min(edge_distance.z, edge_distance.w));
38
+ float border_width = edge == edge_distance.x ? widths.x
39
+ : edge == edge_distance.y ? widths.y : edge == edge_distance.z ? widths.z : widths.w;
40
+ float coordinate = edge == edge_distance.x || edge == edge_distance.z ? local_position.x : local_position.y;
41
+ if (border_width > 0.0 && distance >= -border_width &&
42
+ !(dash == 1.0 && mod(coordinate, 6.0) >= 3.0)) fill = border;
43
+ fill.a *= coverage;
44
+ } else if (center_kind.w == 1.0 || center_kind.w == 2.0) {
45
+ vec2 uv = widths.xy + local_position / primitive_size * widths.zw;
46
+ vec4 sample_color = texture(atlas, uv);
47
+ if (center_kind.w == 1.0) fill.a *= sample_color.r;
48
+ else fill *= sample_color;
49
+ }
50
+ fragment_color = vec4(fill.rgb * fill.a, fill.a);
51
+ }
@@ -0,0 +1,50 @@
1
+ #version 450
2
+
3
+ layout(location = 0) in vec4 instance_bounds;
4
+ layout(location = 1) in vec4 instance_color;
5
+ layout(location = 2) in vec4 instance_secondary;
6
+ layout(location = 3) in vec4 instance_radii;
7
+ layout(location = 4) in vec4 instance_border;
8
+ layout(location = 5) in vec4 instance_widths;
9
+ layout(location = 6) in vec4 instance_gradient;
10
+ layout(location = 7) in vec4 instance_center_kind;
11
+ layout(location = 8) in vec4 instance_matrix;
12
+ layout(location = 9) in vec4 instance_translation;
13
+
14
+ layout(push_constant) uniform Viewport { vec2 size; } viewport;
15
+
16
+ layout(location = 0) out vec2 local_position;
17
+ layout(location = 1) out vec2 primitive_size;
18
+ layout(location = 2) out vec4 color;
19
+ layout(location = 3) out vec4 secondary;
20
+ layout(location = 4) out vec4 radii;
21
+ layout(location = 5) out vec4 border;
22
+ layout(location = 6) out vec4 widths;
23
+ layout(location = 7) out vec4 gradient;
24
+ layout(location = 8) out vec4 center_kind;
25
+ layout(location = 9) out float dash;
26
+
27
+ void main() {
28
+ vec2 corners[4] = vec2[4](vec2(0, 0), vec2(1, 0), vec2(0, 1), vec2(1, 1));
29
+ vec2 corner = corners[gl_VertexIndex];
30
+ vec2 size = instance_bounds.zw;
31
+ vec2 position = instance_bounds.xy + corner * size;
32
+ if (instance_center_kind.w == 3.0) {
33
+ position = gl_VertexIndex == 0 ? instance_bounds.xy
34
+ : gl_VertexIndex == 1 ? instance_bounds.zw : instance_radii.xy;
35
+ }
36
+ position = vec2(instance_matrix.x * position.x + instance_matrix.z * position.y + instance_translation.x,
37
+ instance_matrix.y * position.x + instance_matrix.w * position.y + instance_translation.y);
38
+ gl_Position = vec4(position.x / viewport.size.x * 2.0 - 1.0,
39
+ position.y / viewport.size.y * 2.0 - 1.0, 0.0, 1.0);
40
+ local_position = corner * size;
41
+ primitive_size = size;
42
+ color = instance_color;
43
+ secondary = instance_secondary;
44
+ radii = instance_radii;
45
+ border = instance_border;
46
+ widths = instance_widths;
47
+ gradient = instance_gradient;
48
+ center_kind = instance_center_kind;
49
+ dash = instance_translation.w;
50
+ }