diff --git a/docs/specs/shapes.md b/docs/specs/shapes.md index c4ed478..cdf39d0 100644 --- a/docs/specs/shapes.md +++ b/docs/specs/shapes.md @@ -36,9 +36,9 @@ More formally: $shape \in Scope(modifier)$: * $shape = modifier(shape)$ * compute the total shape transformation by composing all transforms within the shape scope chain: - $$T_{shape} = \prod_{n=0}^{Scope(shape)} Transform(scope_n)$$ + $T_{shape} = \prod_{n=0}^{Scope(shape)} Transform(scope_n)$ * compute the total style transformation by composing all transforms within the style scope chain: - $$T_{style} = \prod_{n=0}^{Scope(style)} Transform(scope_n)$$ + $T_{style} = \prod_{n=0}^{Scope(style)} Transform(scope_n)$ * $Render(shape \times T_{shape}, style \times T_{style})$ @@ -78,20 +78,27 @@ All paths MUST be closed unless specified otherwise in the rendering instruction When instructions call for an equality comparison between two values, implementations MAY consider similar values to be equal to overcome numerical instability. -### Drawing Commands +### Bezier Conversions + +This documents includes algorithms to convert parametric shapes into bezier curves. + +Implementations MAY use different implementations than the algorithms provided here +but the output shape MUST be visually indistinguishable from the output of these algorithms. + +Furthermore, when drawing individual shapes the stroke order and direction is not importand +but implementations of Trim Path MUST follow the stroke order as defined by these algorithms. Drawing instructions will contain the following commands: -* _add vertex_: Adds a vertex to the bezier shape in global coordinates -* _set in tangent_: Sets the cubic tangent to the last added vertex, with coordinates relative to it. If omitted, tangents MUST be $(0, 0)$. -* _set out tangent_: Sets the cubic tangent from the last added vertex, with coordinates relative to it. If omitted, tangents MUST be $(0, 0)$. -* _lerp_: Linearly interpolates two points or scalars by a given amount. +* _Add vertex_: Adds a vertex to the bezier shape in global coordinates +* _Set in tangent_: Sets the cubic tangent to the last added vertex, with coordinates relative to it. If omitted, tangents MUST be $(0, 0)$. +* _Set out tangent_: Sets the cubic tangent from the last added vertex, with coordinates relative to it. If omitted, tangents MUST be $(0, 0)$. ### Approximating Ellipses with Cubic Bezier An elliptical quadrant can be approximated by a cubic bezier segment -with tangents of length $radius \cdot E_t. +with tangents of length $radius * E_t. Where @@ -147,29 +154,29 @@ Hidden shapes (`hd: True`) are ignored, and do not contribute to rendering nor m -An ellipse is drawn from the top quandrant point going clockwise: -$$ -\begin{align*} -radius & = \frac{s}{2} \\ -tangent & = radius \cdot E_t \\ -x & = p.x \\ -y & = p.y \\ -\end{align*} -$$ - -1. Add vertex $(x, y - radius.y)$ -1. Set in tangent $(-tangent.x, 0)$ -1. Set out tangent $(tangent.x, 0)$ -1. Add vertex $(x + radius.x, y)$ -1. Set in tangent $(0, -tangent.y)$ -1. Set out tangent $(0, tangent.y)$ -1. Add vertex $(x, y + radius.y)$ -1. Set in tangent $(tangent.x, 0)$ -1. Set out tangent $(-tangent.x, 0)$ -1. Add vertex $(x - radius.x, y)$ -1. Set in tangent $(0, tangent.y)$ -1. Set out tangent $(0, -tangent.y)$ + +def ellipse(shape: Bezier, p: Vector2D, s: Vector2D): + # An ellipse is drawn from the top quandrant point going clockwise: + radius = s / 2 + tangent = radius * ELLIPSE_CONSTANT + x = p.x + y = p.y + + shape.closed = True + shape.add_vertex(Vector2D(x, y - radius.y)) + shape.set_in_tangent(Vector2D(-tangent.x, 0)) + shape.set_out_tangent(Vector2D(tangent.x, 0)) + shape.add_vertex(Vector2D(x + radius.x, y)) + shape.set_in_tangent(Vector2D(0, -tangent.y)) + shape.set_out_tangent(Vector2D(0, tangent.y)) + shape.add_vertex(Vector2D(x, y + radius.y)) + shape.set_in_tangent(Vector2D(tangent.x, 0)) + shape.set_out_tangent(Vector2D(-tangent.x, 0)) + shape.add_vertex(Vector2D(x - radius.x, y)) + shape.set_in_tangent(Vector2D(0, tangent.y)) + shape.set_out_tangent(Vector2D(0, -tangent.y)) + Implementations MAY use elliptical arcs to render an ellipse. @@ -203,50 +210,50 @@ Implementations MAY use elliptical arcs to render an ellipse. +Rendering algorithm: -Definitions: + +def rectangle(shape: Bezier, p: Vector2D, s: Vector2D, r: float): + left: float = p.x - s.x / 2 + right: float = p.x + s.x / 2 + top: float = p.y - s.y / 2 + bottom: float = p.y + s.y / 2 -$$ -\begin{align*} -left & = p.x - \frac{s.x}{2} \\ -right & = p.x + \frac{s.x}{2} \\ -top & = p.y - \frac{s.y}{2} \\ -bottom & = p.y + \frac{s.y}{2} \\ -\end{align*} -$$ + shape.closed = True -If $r = 0$, then the rectangle is rendered from the top-right going clockwise: + if r <= 0: -1. Add vertex $(right, top)$ -1. Add vertex $(right, bottom)$ -1. Add vertex $(left, bottom)$ -1. Add vertex $(left, top)$ + # The rectangle is rendered from the top-right going clockwise -If $r > 0$, the rounded corners must be taken into account. + shape.add_vertex(Vector2D(right, top)) + shape.add_vertex(Vector2D(right, bottom)) + shape.add_vertex(Vector2D(left, bottom)) + shape.add_vertex(Vector2D(left, top)) -$$ -\begin{align*} -rounded & = \min\left(\frac{s.x}{2}, \frac{s.y}{2}, r\right) \\ -tangent & = rounded \cdot E_t \\ -\end{align*} -$$ + else: -1. Add vertex $(right, top + rounded)$ -1. Set in tangent $(0, -tangent)$ -1. Add vertex $(right, bottom - rounded)$ -1. Set out tangent $(0, tangent)$ -1. Add vertex $(right - rounded, bottom)$ -1. Set in tangent $(tangent, 0)$ -1. Add vertex $(left + rounded, bottom)$ -1. Set out tangent $(-tangent, 0)$ -1. Add vertex $(left, bottom - rounded)$ -1. Set in tangent $(0, tangent)$ -1. Add vertex $(left, top + rounded)$ -1. Set out tangent $(0, -tangent)$ -1. Add vertex $(left + rounded, top)$ -1. Set in tangent $(-tangent, 0)$ -1. Add vertex $(right - rounded, top)$ -1. Set out tangent $(tangent, 0)$ + # Rounded corners must be taken into account + + rounded: float = min(s.x/2, s.y/2, r) + tangent: float = rounded * ELLIPSE_CONSTANT + + shape.add_vertex(Vector2D(right, top + rounded)) + shape.set_in_tangent(Vector2D(0, -tangent)) + shape.add_vertex(Vector2D(right, bottom - rounded)) + shape.set_out_tangent(Vector2D(0, tangent)) + shape.add_vertex(Vector2D(right - rounded, bottom)) + shape.set_in_tangent(Vector2D(tangent, 0)) + shape.add_vertex(Vector2D(left + rounded, bottom)) + shape.set_out_tangent(Vector2D(-tangent, 0)) + shape.add_vertex(Vector2D(left, bottom - rounded)) + shape.set_in_tangent(Vector2D(0, tangent)) + shape.add_vertex(Vector2D(left, top + rounded)) + shape.set_out_tangent(Vector2D(0, -tangent)) + shape.add_vertex(Vector2D(left + rounded, top)) + shape.set_in_tangent(Vector2D(-tangent, 0)) + shape.add_vertex(Vector2D(right - rounded, top)) + shape.set_out_tangent(Vector2D(tangent, 0)) + ![Rectangle rendering guide](../static/img/rect-guide.svg) @@ -335,34 +342,38 @@ $$ - -Definitions: - -$$ -\begin{align*} -points & = \lfloor pt \rceil \\ -\theta & = \frac{\pi}{points} \\ -\alpha & = \frac{\pi}{180} \cdot r \\ -tan_{out} &= \frac{os}{100} \cdot \frac{or \cdot 2 \pi}{points \cdot 4} \\ -tan_{in} &= \frac{is}{100} \cdot \frac{ir \cdot 2 \pi}{points \cdot 4} \\ -\end{align*} -$$ - -1. For $i$ in $[0, points)$ - 1. Let $\beta = -\frac{\pi}{2} + \alpha + i \cdot 2 \dot \theta$ - 1. Let $V_{out} = (or \cdot \cos(\beta), or \cdot \sin(\beta))$ - 1. Add vertex $p + V_{out}$ - 1. If $or \neq 0$, we need to add bezier tangent - 1. Let $T_{out} = (V_{out} \cdot \frac{tan_{out}}{or})$ - 1. Set in tangent $V_{out}$ - 1. Set out tangent $-V_{out}$ - 1. If $sy = 1$, we need to add a vertex towards the inner radius to make a star - 1. Let $V_{in} = (ir \cdot \cos(\beta + \theta), or \cdot \sin(\beta + \theta))$ - 1. Add vertex $p + V_{in}$ - 1. If $ir \neq 0$, we need to add bezier tangent - 1. Let $T_{in} = (V_{in} \cdot \frac{tan_{in}}{or})$ - 1. Set in tangent $V_{in}$ - 1. Set out tangent $-V_{in}$ + +def polystar(shape: Bezier, p: Vector2D, pt: float, r: float, or_: float, os: float, sy: int, ir: float, is_: float): + points: int = int(round(pt)) + alpha: float = -r * math.pi / 180 - math.pi / 2 + theta: float = -math.pi / points + tan_len_out: float = (2 * math.pi * or_) / (4 * points) * (os / 100) + tan_len_in: float = (2 * math.pi * ir) / (4 * points) * (is_ / 100) + + shape.closed = True + + for i in range(points): + beta: float = alpha + i * theta * 2 + v_out: Vector2D = Vector2D(or_ * math.cos(beta), or_ * math.sin(beta)) + shape.add_vertex(p + v_out) + + if os != 0 and or_ != 0: + # We need to add bezier tangents + tan_out: Vector2D = v_out * tan_len_out / or_ + shape.set_in_tangent(Vector2D(-tan_out.y, tan_out.x)) + shape.set_out_tangent(Vector2D(tan_out.y, -tan_out.x)) + + if sy == 1: + # We need to add a vertex towards the inner radius to make a star + v_in: Vector2D = Vector2D(ir * math.cos(beta + theta), ir * math.sin(beta + theta)) + shape.add_vertex(p + v_in) + + if is_ != 0 and ir != 0: + # We need to add bezier tangents + tan_in = v_in * tan_len_in / ir + shape.set_in_tangent(Vector2D(-tan_in.y, tan_in.x)) + shape.set_out_tangent(Vector2D(tan_in.y, -tan_in.x)) +

Grouping

diff --git a/tools/code_processing/loader.py b/tools/code_processing/loader.py new file mode 100644 index 0000000..f112a97 --- /dev/null +++ b/tools/code_processing/loader.py @@ -0,0 +1,22 @@ +from source_translator import SourceCode +from source_translator.langs import cpp, ts +from . import pseudocode + + +language_names = { + "pseudo": "Pseudo-Code", + "py": "Python", + "cpp": "C++", + "ts": "TypeScript", +} + + +def code_to_samples(source): + data = SourceCode(source) + return { + "ast": data.ast, + "pseudo": pseudocode.PseudoCode().convert(data), + "py": source, + "cpp": cpp.CppTranslator().convert(data), + "ts": ts.TypeScriptTranslator().convert(data), + } diff --git a/tools/code_processing/pseudocode.py b/tools/code_processing/pseudocode.py new file mode 100644 index 0000000..990381c --- /dev/null +++ b/tools/code_processing/pseudocode.py @@ -0,0 +1,219 @@ +from source_translator import AstTranslator, IndentationManager +from source_translator.naming import snake_to_lower_camel + + +class PseudoCode(AstTranslator): + ops = { + "Eq": "=", + "NotEq": r"\neq", + "Lt": "<", + "LtE": r"\leq", + "Gt": ">", + "GtE": r"\geq", + "Is": "=", + "IsNot": r"\neq", + "In": r"\in", + "NotIn": r"\notin", + "Add": "+", + "Sub": "-", + "Mult": r"\cdot", + "MatMult": r"\times", + "Div": r"\frac", + "Mod": "%", + "LShift": "<<", + "RShift": ">>", + "BitOr": "|", + "BitXor": "^", + "BitAnd": "&", + "FloorDiv": "//", + "Pow": "^", + "Invert": "~", + "Not": "\\neg", + "UAdd": "+", + "USub": "-", + "And": "\\land", + "Or": "\\lor", + } + + def snake_sentence(self, name, upper): + sentence = name.replace("_", " ") + if upper: + return sentence[0].upper() + sentence[1:] + return sentence + + def expr_attribute(self, object, member): + if object == "shape": + if member == "closed": + return "shape closed" + return self.snake_sentence(member, True) + if object == "math": + return "\\" + member + return "%s.%s" % (object, member) + + def begin_if(self, expr): + self.push_code("If $%s$" % expr) + + def end_block(self): + pass + + def begin_else(self): + self.push_code("Otherwise") + + def declare(self, target, type, value, ast_value): + self.push_code("$%s \\coloneq %s$" % (self.decorate_name(target), value)) + + def format_comment(self, value): + if len(value) == 0: + self.push_code("") + for line in value: + self.push_code(line) + + def expression_statement(self, expr): + if " " in expr or "$" in expr: + self.push_code(expr) + else: + self.push_code("$%s$" % expr) + + def decorate_name(self, name): + type = self.get_var_type(name) + if type.startswith("\\mathbb{R}^"): + return "\\vec{%s}" % name + return name + + def expr_func(self, name, args): + if name == "int": + return args[0] + + if name == "round": + return r"\lfloor %s \rceil" % ", ".join(args) + + if name == "floor": + return r"\lfloor %s \rfloor" % ", ".join(args) + + if name == "ceil": + return r"\lceil %s \rceil" % ", ".join(args) + + if name == "range": + start = "0" + end = "0" + if len(args) == 1: + end = args[0] + elif len(args) == 2: + start, end = args + else: + raise NotImplementedError + + return "[%s, %s)" % (start, end) + + is_sentence = " " in name + if name == "Vector2D": + name = "" + code = name + if is_sentence: + code += " $" + else: + code += r"\left(" + code += ", ".join(args) + if is_sentence: + code += "$" + else: + code += r"\right)" + return code + + def convert_name(self, name, annotation): + if annotation: + if name == "float": + return "\\mathbb{R}" + elif name == "int": + return "\\mathbb{Z}" + elif name == "Vector2D": + return "\\mathbb{R}^2" + + if name in ("min", "max"): + return "\\" + name + if name == "ELLIPSE_CONSTANT": + return "E_t" + if name in ("alpha", "beta", "theta"): + return "\\" + name + + name = name.strip("_") + chunks = name.rsplit("_", 1) + name = snake_to_lower_camel(chunks[0]) + if len(chunks) == 2: + name += "_{%s}" % chunks[1] + return self.decorate_name(name) + + def convert_constant(self, value, annotation): + if value is None: + return "nil" + if isinstance(value, bool): + return str(value).lower() + return repr(value) + + def expr_binop(self, op, *operands): + if op == "\\frac": + return "%s{%s}{%s}" % (op, operands[0], operands[1]) + return (" %s " % op).join(operands) + + def assign(self, targets, value): + for target in targets: + if " " in target: + if value == "true": + self.push_code("Set %s" % (target)) + elif value == "false": + + self.push_code("Unset %s" % (target)) + else: + self.push_code("Set %s to %s" % (target, value)) + else: + self.push_code("$%s \\coloneq %s$" % (target, value)) + + def function_def(self, name, args, returns, body, is_async, is_method, is_getter): + self.push_code(self.snake_sentence(name, True)) + + with IndentationManager(self, False): + args_start = 0 + if is_method and len(args.args) > 0 and args.args[0].arg in ("self", "cls"): + args_start = 1 + + if len(args.args) > args_start and args.args[args_start].arg == "shape": + args_start += 1 + + if len(args.args) > args_start: + self.push_code("Inputs:") + + with IndentationManager(self, False): + for i in range(args_start, len(args.args)): + name = self.convert_name(args.args[i].arg, False) + if args.args[i].annotation: + type = self.expression_to_string(args.args[i].annotation, True) + else: + type = None + self.var_add(name, type) + arg = "$" + self.decorate_name(name) + if type: + arg += " \\in " + type + + reverse_i = len(args.args) - i + if reverse_i <= len(args.defaults): + arg += " = %s" % self.expression_to_string(args.defaults[-reverse_i]) + arg += "$" + self.push_code(arg) + self.push_code("") + + with IndentationManager(self, False): + self.convert_ast(body) + + def expr_compare(self, value, annotation): + expr = self.expression_to_string(value.left, annotation) + for cmp, op in zip(value.comparators, value.ops): + expr += " " + self.expression_to_string(op, annotation) + expr += " " + self.expression_to_string(cmp, annotation) + return expr + + def begin_for(self, target, iter, is_async): + code_start = "For each $%s$ in $%s$" % (target, iter) + self.push_code(code_start) + + def convert_line_comment(self, comment): + return comment or "" diff --git a/tools/lottie_markdown.py b/tools/lottie_markdown.py index 9e1cc62..5dd8d76 100644 --- a/tools/lottie_markdown.py +++ b/tools/lottie_markdown.py @@ -14,6 +14,7 @@ from schema_tools.schema import Schema, SchemaPath from schema_tools import type_info +from code_processing.loader import code_to_samples, language_names docs_path = Path(__file__).parent.parent / "docs" @@ -1192,6 +1193,54 @@ def handleMatch(self, match, data): return span, match.start(0), match.end(0) +class Algorithm(BlockProcessor): + def test(self, parent, block): + return block.startswith("" not in raw_string: + raw_string += "\n\n" + blocks.pop(0) + + source = raw_string[raw_string.find('\n'):raw_string.rfind('\n')] + + samples = code_to_samples(source) + + container = etree.SubElement(parent, "div", {"class": "algorithm"}) + + selector = etree.SubElement(container, "select", { + "onchange": """this.parentElement.querySelectorAll("pre").forEach( + (e, i) => e.style.display = i == this.value ? "block" : "none" + )""" + }) + + samples.pop("ast") + + for index, (lang, code) in enumerate(samples.items()): + self.render_code(container, index, lang, code, selector) + + return True + + def render_code(self, parent, index, language, code, selector): + if language == "pseudo": + self.render_pseudocode(parent, code) + else: + pre = etree.SubElement(parent, "pre", {"style": "display: none"}) + etree.SubElement(pre, "code", {"class": "language-%s hljs" % language}).text = AtomicString(code) + + option = etree.SubElement(selector, "option") + option.attrib["value"] = str(index) + option.text = language_names[language] + + def render_pseudocode(self, parent, pseudo: str): + pre = etree.SubElement(parent, "pre") + for line in pseudo.splitlines(): + span = etree.SubElement(pre, "span") + span.text = line + span.tail = "\n" + + class LottieExtension(Extension): def extendMarkdown(self, md: Markdown): ts = typed_schema(Schema.load(docs_path / "lottie.schema.json")) @@ -1212,7 +1261,7 @@ def extendMarkdown(self, md: Markdown): md.parser.blockprocessors.register( RawHTML( md.parser, - ["lottie", "lottie-playground"] + ["lottie", "lottie-playground", "algorithm"] ), "raw_heading", 100 @@ -1222,6 +1271,7 @@ def extendMarkdown(self, md: Markdown): md.parser.blockprocessors.register(SchemaObject(md, ts), "schema_object", 175) md.parser.blockprocessors.register(SchemaEnum(md, ts), "schema_enum", 175) md.parser.blockprocessors.register(EditorExample(md.parser), "editor_example", 175) + md.parser.blockprocessors.register(Algorithm(md.parser), "algorithm", 175) def makeExtension(**kwargs): diff --git a/tools/render_shape.py b/tools/render_shape.py new file mode 100755 index 0000000..a0aab80 --- /dev/null +++ b/tools/render_shape.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +import sys +import math +import inspect +import argparse +import lottie +from code_processing.loader import code_to_samples, SourceCode + + +class Bezier(lottie.objects.bezier.BezierView): + def __init__(self): + super().__init__(lottie.objects.bezier.Bezier(), False) + + def add_vertex(self, p: lottie.NVector): + self.append(p) + + def set_in_tangent(self, p: lottie.NVector): + self[-1].in_tangent = p + + def set_out_tangent(self, p: lottie.NVector): + self[-1].out_tangent = p + + @property + def closed(self): + return self.bezier.closed + + @closed.setter + def closed(self, v): + self.bezier.closed = v + + +exec_globals = { + "Color": lottie.Color, + "Vector2D": lottie.NVector, + "ELLIPSE_CONSTANT": 0.5519150244935105707435627, + "Bezier": Bezier, + "math": math, +} + +default_args = { + "NVector": lottie.NVector(200, 200), + "float": 50, + "int": 5, +} + + +def render_shape(func, args): + anim = lottie.objects.Animation() + lay = lottie.objects.layers.ShapeLayer() + anim.add_layer(lay) + lay.in_point = anim.in_point + lay.out_point = anim.out_point + + shape = Bezier() + func(shape, *args) + lay.shapes.append(lottie.objects.shapes.Path(shape.bezier)) + lay.shapes.append(lottie.objects.shapes.Stroke(lottie.Color(1, 0.5, 0), 6)) + lay.shapes.append(lottie.objects.shapes.Fill(lottie.Color(1, 1, 0))) + return anim + + +def main(argv): + if argv.input: + with open(argv.input) as f: + code = f.read() + else: + print("Type code (end with ^D)") + code = sys.stdin.read() + + if argv.view_code: + data = code_to_samples(code) + print(data[argv.view_code]) + parsed = data["ast"] + else: + parsed = SourceCode(code).ast + + local = {} + exec(compile(parsed, "", "exec"), exec_globals, local) + + default_func = next(iter(local.keys())) + + if argv.func is not None: + func_name = argv.func + else: + sys.stdout.write("Function name [%s]: " % default_func) + sys.stdout.flush() + func_name = sys.stdin.readline().strip() + + func = local[func_name or default_func] + + arg_spec = inspect.getfullargspec(func) + + given_args = {} + + if argv.args: + for i in range(0, len(argv.args), 2): + given_args[argv.args[i]] = argv.args[i + 1] + + args = [] + for i, arg in enumerate(arg_spec.args): + if i == 0 and arg == "shape": + continue + annot = arg_spec.annotations[arg] + typename = annot.__name__ + value = default_args[typename] + if arg in given_args: + value_raw = given_args[arg] + else: + sys.stdout.write("%s (%s) [%s]: " % (arg, typename, value)) + sys.stdout.flush() + value_raw = sys.stdin.readline().strip() + if value_raw: + value = annot(*eval("[%s]" % value_raw)) + args.append(value) + + anim = render_shape(func, args) + + lottie.exporters.core.export_embedded_html(anim, "/tmp/out.html") + + +parser = argparse.ArgumentParser() +parser.add_argument("--input", "-i", help="File for the code input") +parser.add_argument("--func", "-f", default=None, help="Function name") +parser.add_argument("--args", nargs="+", help="Argument values for the function") +parser.add_argument("--view-code", "-c", metavar="lang", help="Display rendered code") + +if __name__ == "__main__": + args = parser.parse_args() + main(args) diff --git a/tools/requirements.txt b/tools/requirements.txt index 30f5968..aaef686 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -4,6 +4,7 @@ mkdocs-cinder==1.2.0 git+https://github.com/mbasaglia/mkdocs-print-site-plugin.git@rename-page graphviz==0.20.1 latex2mathml==3.77.0 +source-translator==1.0.0 # Used for link validations lxml==4.9.3 # Versioning diff --git a/tools/theme/css/style.css b/tools/theme/css/style.css index 89d6d13..c556cec 100644 --- a/tools/theme/css/style.css +++ b/tools/theme/css/style.css @@ -107,9 +107,12 @@ canvas.bezier-editor { } /* mathml */ +math[display="block"] { + margin: 1em 0; +} math { - margin: 1em 0; + font-size: 20px; } mtd[columnalign=right] { @@ -121,3 +124,15 @@ mtd[columnalign=left] { text-align: -webkit-left; text-align: -moz-left; } + +/* algorithm */ + +.algorithm { + position: relative; +} + +.algorithm select { + position: absolute; + top: 0; + right: 0; +}