Module: Ruby2D::Renderable
- Includes:
- Interactive
- Included in:
- BitmapText, Canvas, Circle, Ellipse, Image, Line, Polygon, Polyline, Quad, Text, Tileset, Triangle
- Defined in:
- lib/ruby2d/renderable.rb
Overview
Shared behavior for all renderable objects
Constant Summary
Constants included from Interactive
Interactive::OBJECT_EVENTS, Interactive::OBJECT_EVENT_FILTER_PREDICATES
Instance Attribute Summary collapse
-
#color ⇒ Object
(also: #colour)
Returns the value of attribute color.
-
#height ⇒ Object
readonly
Returns the value of attribute height.
-
#padding_bottom ⇒ Object
Returns the value of attribute padding_bottom.
-
#padding_left ⇒ Object
Returns the value of attribute padding_left.
-
#padding_right ⇒ Object
Returns the value of attribute padding_right.
-
#padding_top ⇒ Object
Returns the value of attribute padding_top.
-
#visible ⇒ Object
(also: #visible?)
Returns the value of attribute visible.
-
#width ⇒ Object
readonly
Returns the value of attribute width.
-
#x ⇒ Object
readonly
Returns the value of attribute x.
-
#x_align ⇒ Object
Returns the value of attribute x_align.
-
#y ⇒ Object
readonly
Returns the value of attribute y.
-
#y_align ⇒ Object
Returns the value of attribute y_align.
-
#z ⇒ Object
Returns the value of attribute z.
Class Method Summary collapse
-
.flatten_color(input, vertices, opacity = nil, label: nil) ⇒ Object
Normalize any color input into a flat RGBA array with
verticesentries. -
.flatten_per_vertex(input, vertices, opacity) ⇒ Object
Flatten a per-vertex color array straight into the
vertices×4 float array, skippingColor::Setentirely. -
.flatten_points(points) ⇒ Object
Validate an array of
[x, y]pairs and flatten it to a flat float array in one pass. -
.flatten_resolved_color(c, vertices, opacity = nil, label: nil) ⇒ Object
Flatten an already-resolved
Color/Color::Setinto thevertices×4 float array. -
.resolve_color_or_default(input, vertex_count, label: nil) ⇒ Object
Resolve an input color allowing a Color::Set of exactly
vertex_countentries or a single color. -
.resolve_single_color(input) ⇒ Object
Resolve an input color to a single Color.
Instance Method Summary collapse
-
#_alignment_anchor_dx ⇒ Object
Offset from the bounding-box top-left (what
_resolve_alignmentcomputes) to the shape's position anchor, split into x/y components to avoid boxing a throwaway pair each aligned frame. - #_alignment_anchor_dy ⇒ Object
-
#_apply_padding(padding, top, right, bottom, left) ⇒ Object
Resolve construction-time padding kwargs into per-edge values.
-
#_extract_alignment(x, y) ⇒ Object
Extract symbolic alignment from a constructor (x, y) pair, store the intent, and return numeric placeholders to use until the first draw.
-
#_point_in_polygon?(coords, px, py) ⇒ Boolean
Even-odd ray-cast point-in-polygon test over a flat
[x0, y0, x1, y1, ...]coordinate array. -
#_point_on_segment?(px, py, x1, y1, x2, y2, half_sq) ⇒ Boolean
Hit-test a point against the stroked band of segment (x1, y1)-(x2, y2): true when (px, py) lies within the rectangle the stroke actually draws.
-
#_render_scene ⇒ Object
Scene-graph draw hook, called once per frame per visible object.
-
#_require_numeric_position(axis, value) ⇒ Object
Reject a non-numeric position on a shape with no single anchor to align.
-
#_resolve_alignment ⇒ Object
Resolve symbolic alignment to numeric x/y.
-
#_unrotate(px, py) ⇒ Object
Map a query point into this object's unrotated coordinate frame.
-
#_validate_dimensions(**dims) ⇒ Object
Reject negative construction-time dimensions (width, height, radius, …).
-
#add ⇒ Object
Add the object to the window's scene graph.
- #colour=(color) ⇒ Object
-
#contains?(x, y) ⇒ Boolean
Check if the object contains the given point.
-
#hide ⇒ Object
Mark the object hidden.
-
#opacity ⇒ Object
Get the opacity (alpha) of the object's fill color.
-
#opacity=(value) ⇒ Object
Set the opacity (alpha) of the object's color.
-
#padding=(value) ⇒ Object
Set all four padding edges to the same value.
-
#remove ⇒ Object
Remove the object from the window's scene graph.
-
#show ⇒ Object
Mark the object visible.
Methods included from Interactive
#_fire_event, #interactive?, #off, #on
Instance Attribute Details
#color ⇒ Object Also known as: colour
Returns the value of attribute color.
171 172 173 |
# File 'lib/ruby2d/renderable.rb', line 171 def color @color end |
#height ⇒ Object (readonly)
Returns the value of attribute height.
171 172 173 |
# File 'lib/ruby2d/renderable.rb', line 171 def height @height end |
#padding_bottom ⇒ Object
Returns the value of attribute padding_bottom.
172 173 174 |
# File 'lib/ruby2d/renderable.rb', line 172 def padding_bottom @padding_bottom end |
#padding_left ⇒ Object
Returns the value of attribute padding_left.
172 173 174 |
# File 'lib/ruby2d/renderable.rb', line 172 def padding_left @padding_left end |
#padding_right ⇒ Object
Returns the value of attribute padding_right.
172 173 174 |
# File 'lib/ruby2d/renderable.rb', line 172 def padding_right @padding_right end |
#padding_top ⇒ Object
Returns the value of attribute padding_top.
172 173 174 |
# File 'lib/ruby2d/renderable.rb', line 172 def padding_top @padding_top end |
#visible ⇒ Object Also known as: visible?
Returns the value of attribute visible.
172 173 174 |
# File 'lib/ruby2d/renderable.rb', line 172 def visible @visible end |
#width ⇒ Object (readonly)
Returns the value of attribute width.
171 172 173 |
# File 'lib/ruby2d/renderable.rb', line 171 def width @width end |
#x ⇒ Object (readonly)
Returns the value of attribute x.
171 172 173 |
# File 'lib/ruby2d/renderable.rb', line 171 def x @x end |
#x_align ⇒ Object
Returns the value of attribute x_align.
171 172 173 |
# File 'lib/ruby2d/renderable.rb', line 171 def x_align @x_align end |
#y ⇒ Object (readonly)
Returns the value of attribute y.
171 172 173 |
# File 'lib/ruby2d/renderable.rb', line 171 def y @y end |
#y_align ⇒ Object
Returns the value of attribute y_align.
171 172 173 |
# File 'lib/ruby2d/renderable.rb', line 171 def y_align @y_align end |
#z ⇒ Object
Returns the value of attribute z.
171 172 173 |
# File 'lib/ruby2d/renderable.rb', line 171 def z @z end |
Class Method Details
.flatten_color(input, vertices, opacity = nil, label: nil) ⇒ Object
Normalize any color input into a flat RGBA array with vertices entries.
Accepts names, hex, [r,g,b,a], Color, Color::Set, per-vertex arrays, or nil
(which defaults to white). opacity, if given, overrides the alpha of each
vertex. label: optionally supplies a class label used in the error message.
Used by class-level .render methods.
38 39 40 41 42 43 44 45 46 |
# File 'lib/ruby2d/renderable.rb', line 38 def self.flatten_color(input, vertices, opacity = nil, label: nil) fast = flatten_per_vertex(input, vertices, opacity) return fast if fast # `for_render`'s shared cached instance is safe here: this method only # reads the color into a flat float array, so it can never escape. c = Color.for_render(input.nil? ? 'white' : input) flatten_resolved_color(c, vertices, opacity, label: label) end |
.flatten_per_vertex(input, vertices, opacity) ⇒ Object
Flatten a per-vertex color array straight into the vertices×4 float
array, skipping Color::Set entirely. The general path builds a Set,
which allocates and parses one Color per vertex via Color.new —
bypassing the render cache that makes the single-color path cheap. A
per-frame .render pays that in full every frame, and for the hex
strings the examples favor it is the most expensive color form there
is. Numeric tuples are read directly; everything else resolves through
Color.for_render, whose shared instance is safe here because it is
read into floats immediately and never stored.
Returns nil — falling back to the general path — for anything
unusual: a mismatched length, an out-of-range channel, an element that
isn't a valid color. Those still raise and warn from the one place
below, so this stays a pure fast path and never the error authority.
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/ruby2d/renderable.rb', line 84 def self.flatten_per_vertex(input, vertices, opacity) return nil unless input.is_a?(Array) && input.length == vertices && vertices.positive? # A flat `[r, g, b(, a)]` is a single color for every vertex, not a # list of them — its first element is the only Numeric case here. return nil if input[0].is_a?(Numeric) return nil if opacity.is_a?(Array) && opacity.length != vertices opacity_array = opacity.is_a?(Array) flat = Array.new(vertices * 4) i = 0 while i < vertices el = input[i] if el.instance_of?(Array) # `instance_of?` mirrors `Color.valid?` — an Array subclass is not # the plain-array form and must take the general path. n = el.length return nil unless n == 3 || n == 4 r = el[0] g = el[1] b = el[2] a = n == 4 ? el[3] : 1.0 return nil unless r.is_a?(Numeric) && g.is_a?(Numeric) && b.is_a?(Numeric) && a.is_a?(Numeric) # Out of range is `Color#channel`'s business: it warns and clamps. return nil unless r >= 0.0 && r <= 1.0 && g >= 0.0 && g <= 1.0 && b >= 0.0 && b <= 1.0 && a >= 0.0 && a <= 1.0 r = r.to_f g = g.to_f b = b.to_f a = a.to_f elsif el.is_a?(Color) r = el.r g = el.g b = el.b a = el.a else # Validate first so an invalid element still raises from the # general path, with the message it has always produced. return nil unless Color.valid?(el) c = Color.for_render(el) r = c.r g = c.g b = c.b a = c.a end j = i * 4 flat[j] = r flat[j + 1] = g flat[j + 2] = b flat[j + 3] = (opacity_array ? opacity[i] : opacity) || a i += 1 end flat end |
.flatten_points(points) ⇒ Object
Validate an array of [x, y] pairs and flatten it to a flat float
array in one pass. Replaces an all? validation followed by a
flat_map, which walked the points twice and allocated a throwaway
pair per vertex; on a per-frame .render of a few thousand points that
overhead is comparable to the draw itself. while, not blocks — this
is a per-draw-call path and block calls dominate on wasm mruby.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/ruby2d/renderable.rb', line 54 def self.flatten_points(points) n = points.length coords = Array.new(n * 2) i = 0 while i < n point = points[i] raise ArgumentError, 'points must be an array of [x, y] pairs' \ unless point.is_a?(Array) && point.length == 2 coords[i * 2] = point[0].to_f coords[i * 2 + 1] = point[1].to_f i += 1 end coords end |
.flatten_resolved_color(c, vertices, opacity = nil, label: nil) ⇒ Object
Flatten an already-resolved Color/Color::Set into the vertices×4
float array. Split from flatten_color so the immediate-mode .render
paths can resolve the color once — to choose the single-color fast path —
without parsing it a second time when they fall back to the per-vertex array.
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/ruby2d/renderable.rb', line 147 def self.flatten_resolved_color(c, vertices, opacity = nil, label: nil) if c.is_a?(Color::Set) && c.length != vertices prefix = label ? "`#{label}` " : '' raise ArgumentError, "#{prefix}requires #{vertices} colors, one for each vertex. #{c.length} were given." end if opacity.is_a?(Array) && opacity.length != vertices prefix = label ? "`#{label}` " : '' raise ArgumentError, "#{prefix}requires #{vertices} opacity values, one for each vertex. #{opacity.length} were given." end flat = Array.new(vertices * 4) opacity_array = opacity.is_a?(Array) vertices.times do |i| col = c.vertex(i) flat[i * 4] = col.r flat[i * 4 + 1] = col.g flat[i * 4 + 2] = col.b flat[i * 4 + 3] = (opacity_array ? opacity[i] : opacity) || col.a end flat end |
.resolve_color_or_default(input, vertex_count, label: nil) ⇒ Object
Resolve an input color allowing a Color::Set of exactly vertex_count
entries or a single color. nil returns white. Raises ArgumentError for
a Color::Set of the wrong length. Used by shapes that support per-vertex
gradients along their outline or fill.
22 23 24 25 26 27 28 29 30 31 |
# File 'lib/ruby2d/renderable.rb', line 22 def self.resolve_color_or_default(input, vertex_count, label: nil) return Color.new('white') if input.nil? c = Color.set(input) if c.is_a?(Color::Set) && c.length != vertex_count prefix = label ? "`#{label}` " : '' raise ArgumentError, "#{prefix}requires #{vertex_count} colors, one for each vertex. #{c.length} were given." end c end |
.resolve_single_color(input) ⇒ Object
Resolve an input color to a single Color. If given a per-vertex array or Color::Set, returns the first color. nil returns nil. Used to derive a single stroke color from a fill that may be per-vertex.
12 13 14 15 16 |
# File 'lib/ruby2d/renderable.rb', line 12 def self.resolve_single_color(input) return nil if input.nil? c = Color.set(input) c.is_a?(Color::Set) ? Color.new(c.first) : c end |
Instance Method Details
#_alignment_anchor_dx ⇒ Object
Offset from the bounding-box top-left (what _resolve_alignment computes)
to the shape's position anchor, split into x/y components to avoid boxing a
throwaway pair each aligned frame. Top-left-anchored shapes (Rectangle,
Image, Text, …) need none; center-anchored shapes override to half their
extent so :left hugs the wall with the bounding box, like a rectangle.
236 237 238 |
# File 'lib/ruby2d/renderable.rb', line 236 def _alignment_anchor_dx 0 end |
#_alignment_anchor_dy ⇒ Object
240 241 242 |
# File 'lib/ruby2d/renderable.rb', line 240 def _alignment_anchor_dy 0 end |
#_apply_padding(padding, top, right, bottom, left) ⇒ Object
Resolve construction-time padding kwargs into per-edge values. The
uniform padding: seeds all four edges; per-edge kwargs override
individual slots. Called from each shape's initialize after
_extract_alignment.
248 249 250 251 252 253 254 |
# File 'lib/ruby2d/renderable.rb', line 248 def _apply_padding(padding, top, right, bottom, left) base = padding || 0 @padding_top = top || base @padding_right = right || base @padding_bottom = bottom || base @padding_left = left || base end |
#_extract_alignment(x, y) ⇒ Object
Extract symbolic alignment from a constructor (x, y) pair, store the intent, and return numeric placeholders to use until the first draw.
271 272 273 274 275 |
# File 'lib/ruby2d/renderable.rb', line 271 def _extract_alignment(x, y) self.x_align = x if x.is_a?(Symbol) self.y_align = y if y.is_a?(Symbol) [x.is_a?(Symbol) ? 0 : x, y.is_a?(Symbol) ? 0 : y] end |
#_point_in_polygon?(coords, px, py) ⇒ Boolean
Even-odd ray-cast point-in-polygon test over a flat [x0, y0, x1, y1, ...]
coordinate array. This is the shared hit-test for every filled polygonal
shape (Triangle, Quad, Polygon) so contains? means "inside the
rendered fill" consistently — matching the fill for simple polygons (convex
and concave). Self-intersecting input isn't fully supported by the fill
renderer, so the test can diverge from the drawn pixels there. The boundary
is half-open (top/right edges read as outside), as in rasterization.
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 |
# File 'lib/ruby2d/renderable.rb', line 393 def _point_in_polygon?(coords, px, py) n = coords.length / 2 inside = false j = n - 1 n.times do |i| xi = coords[i * 2]; yi = coords[i * 2 + 1] xj = coords[j * 2]; yj = coords[j * 2 + 1] if (yi > py) != (yj > py) && px < (xj - xi) * (py - yi) / (yj - yi).to_f + xi inside = !inside end j = i end inside end |
#_point_on_segment?(px, py, x1, y1, x2, y2, half_sq) ⇒ Boolean
Hit-test a point against the stroked band of segment (x1, y1)-(x2, y2):
true when (px, py) lies within the rectangle the stroke actually draws.
half_sq is the squared half-stroke-width tolerance. The point must
project onto the segment (0 <= t <= 1) and fall within the half-width
perpendicular, so the hit region matches the drawn (butt-capped) rectangle
and does not overhang the ends. Shared stroke hit-test for Line and
Polyline; compares squared distances to skip the square root on this
per-event, per-segment path.
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 |
# File 'lib/ruby2d/renderable.rb', line 417 def _point_on_segment?(px, py, x1, y1, x2, y2, half_sq) dx = x2 - x1 dy = y2 - y1 len_sq = dx * dx + dy * dy return false if len_sq.zero? # zero-length segment draws nothing # fdiv (not /) so integer coordinates don't trigger integer floor # division, which would snap t to 0/1 and mis-measure the projection. t = ((px - x1) * dx + (py - y1) * dy).fdiv(len_sq) return false if t < 0.0 || t > 1.0 # past an end — outside the drawn rect cx = x1 + t * dx cy = y1 + t * dy ex = px - cx; ey = py - cy ex * ex + ey * ey <= half_sq end |
#_render_scene ⇒ Object
Scene-graph draw hook, called once per frame per visible object. This
fallback forwards to render so a custom renderable only has to define
render; every built-in shape overrides or aliases it to skip the
keyword handling of its public render, which costs ~5µs per call on
wasm mruby even when no keywords are passed. Public — like the other
underscore-prefixed internals — so the scene loop can call it directly:
a send there costs real time at thousands of objects per frame. Shapes
that alias it from a private render re-publicize the alias with
public :_render_scene, since aliases inherit the original visibility.
337 338 339 |
# File 'lib/ruby2d/renderable.rb', line 337 def _render_scene render end |
#_require_numeric_position(axis, value) ⇒ Object
Reject a non-numeric position on a shape with no single anchor to align.
Bounding-box shapes (Image, Text, Rectangle, Circle, Ellipse) accept a
symbol like :center as alignment intent; the centroid-anchored vertex
shapes (Triangle, Quad, Polygon, Polyline) and the pixel-buffer Canvas have
nothing to align, so a symbol there is a setup mistake. Raise a clear error
rather than let it reach the centroid arithmetic (a bare Symbol#-
NoMethodError) or the native renderer (a cryptic type error at draw time).
Returns the value so a setter can validate and assign in one expression.
285 286 287 288 289 290 291 |
# File 'lib/ruby2d/renderable.rb', line 285 def _require_numeric_position(axis, value) return value if value.is_a?(Numeric) raise Error, "#{self.class} #{axis} must be a number; #{self.class} doesn't support " \ "symbolic alignment (e.g. #{axis}: :center) — use a bounding-box shape " \ 'like Text, Image, or Rectangle for that' end |
#_resolve_alignment ⇒ Object
Resolve symbolic alignment to numeric x/y. Called from the render path of
each shape that opts into alignment. Requires the window to be open —
Window.viewport_width and viewport_height are only correct after
show starts. The case computes a bounding-box top-left position;
_alignment_anchor_dx/_dy then shift it to the shape's own anchor
(zero for top-left shapes, half-extent for center-anchored Circle/Ellipse).
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'lib/ruby2d/renderable.rb', line 201 def _resolve_alignment return unless @x_align || @y_align dx = _alignment_anchor_dx dy = _alignment_anchor_dy @_resolving_alignment = true begin if @x_align span = Window. self.x = (case @x_align when :left then (@padding_left || 0) when :center then (span - width) / 2.0 when :right then span - width - (@padding_right || 0) else raise ArgumentError, "Unknown x alignment: #{@x_align.inspect}" end) + dx end if @y_align span = Window. self.y = (case @y_align when :top then (@padding_top || 0) when :center then (span - height) / 2.0 when :bottom then span - height - (@padding_bottom || 0) else raise ArgumentError, "Unknown y alignment: #{@y_align.inspect}" end) + dy end ensure @_resolving_alignment = false end end |
#_unrotate(px, py) ⇒ Object
Map a query point into this object's unrotated coordinate frame. Each
shape's render rotates geometry about (rx, ry) by @rotate degrees
before drawing; contains? calls this first so hit-testing matches what
the user sees. Returns the point unchanged when there's no rotation (or
the object has no rotation pivot, e.g. BitmapText).
375 376 377 378 379 380 381 382 383 384 |
# File 'lib/ruby2d/renderable.rb', line 375 def _unrotate(px, py) rotate = instance_variable_defined?(:@rotate) ? @rotate : nil return [px, py] if rotate.nil? || rotate == 0 cx = rx; cy = ry rad = -rotate * Math::PI / 180.0 sa = Math.sin(rad); ca = Math.cos(rad) dx = px - cx; dy = py - cy [dx * ca - dy * sa + cx, dx * sa + dy * ca + cy] end |
#_validate_dimensions(**dims) ⇒ Object
Reject negative construction-time dimensions (width, height, radius, …).
A negative extent is a setup mistake: it renders but disagrees with hit
testing, since contains? assumes positive geometry. Zero is allowed
(collapse-to-point). Runtime setters are deliberately left unguarded so an
animation whose size momentarily dips below zero degrades to nothing for
that frame rather than crashing the app — see USAGE.md.
262 263 264 265 266 267 |
# File 'lib/ruby2d/renderable.rb', line 262 def _validate_dimensions(**dims) dims.each do |name, value| next unless value.is_a?(Numeric) && value.negative? raise ArgumentError, "#{self.class} #{name} must be zero or positive, got #{value}" end end |
#add ⇒ Object
Add the object to the window's scene graph. This governs iteration and
z-ordering, not visibility. Use #show / #hide (or the visible accessor)
to toggle frame-by-frame drawing without affecting scene-graph membership.
306 307 308 |
# File 'lib/ruby2d/renderable.rb', line 306 def add Window.add(self) end |
#colour=(color) ⇒ Object
349 350 351 |
# File 'lib/ruby2d/renderable.rb', line 349 def colour=(color) self.color = color end |
#contains?(x, y) ⇒ Boolean
Check if the object contains the given point
435 436 437 438 |
# File 'lib/ruby2d/renderable.rb', line 435 def contains?(x, y) x, y = _unrotate(x, y) x >= @x && x <= (@x + @width) && y >= @y && y <= (@y + @height) end |
#hide ⇒ Object
Mark the object hidden. Stays in the scene graph (and contains?/events continue to work). Visually equivalent to opacity 0 but with zero draw cost.
324 325 326 |
# File 'lib/ruby2d/renderable.rb', line 324 def hide @visible = false end |
#opacity ⇒ Object
Get the opacity (alpha) of the object's fill color. For per-vertex
Color::Set fills, returns the alpha of the first color.
355 356 357 |
# File 'lib/ruby2d/renderable.rb', line 355 def opacity @color&.opacity end |
#opacity=(value) ⇒ Object
Set the opacity (alpha) of the object's color. Fades the fill and, on
shapes that have one, the stroke too — matching how a construction-time
opacity: applies to both. For per-vertex Color::Set colors, sets every
vertex's alpha to the same value. To fade fill and stroke independently,
set each color's opacity directly: obj.color.opacity = a and
obj.stroke_color.opacity = b.
365 366 367 368 |
# File 'lib/ruby2d/renderable.rb', line 365 def opacity=(value) @color.opacity = value @stroke_color.opacity = value if instance_variable_defined?(:@stroke_color) && @stroke_color end |
#padding=(value) ⇒ Object
Set all four padding edges to the same value. Padding is the distance,
in pixels, between the object and the window edge it's anchored to.
No effect on :center-aligned axes or un-aligned axes.
179 180 181 |
# File 'lib/ruby2d/renderable.rb', line 179 def padding=(value) @padding_top = @padding_right = @padding_bottom = @padding_left = value end |
#remove ⇒ Object
Remove the object from the window's scene graph.
311 312 313 |
# File 'lib/ruby2d/renderable.rb', line 311 def remove Window.remove(self) end |
#show ⇒ Object
Mark the object visible. Preserves scene-graph position and z-order; the window's render loop will draw it on subsequent frames.
317 318 319 |
# File 'lib/ruby2d/renderable.rb', line 317 def show @visible = true end |