Changeset View
Standalone View
source/blender/blenlib/BLI_color.hh
| Show All 16 Lines | |||||
| #pragma once | #pragma once | ||||
| #include <iostream> | #include <iostream> | ||||
| #include "BLI_math_color.h" | #include "BLI_math_color.h" | ||||
| namespace blender { | namespace blender { | ||||
| struct Color4f { | /** | ||||
| float r, g, b, a; | * CPP based color structures. | ||||
| * | |||||
| * Strongly typed color storage structures with space and alpha association. | |||||
| * Will increase readability and visibility of typically mistakes when | |||||
JacquesLucke: typo (`typically`) | |||||
| * working with colors. | |||||
| * | |||||
| * The storage structs can hold 4 channels (r, g, b and a). | |||||
| * | |||||
| * Usage: | |||||
| * | |||||
| * Convert an srgb byte color to a linearrgb premultiplied. | |||||
Done Inline Actionstypo (an) JacquesLucke: typo (`an`) | |||||
| * ``` | |||||
| * ColorSrgb4b srgb_color; | |||||
| * ColorSceneLinear4f<eAlpha::Premultiplied> linearrgb_color = | |||||
| * BLI_color_convert_to_linear(srgb_color).to_premultiplied_alpha(); | |||||
Done Inline ActionsThis seems to be outdated, the BLI_color_convert_to_linear function does not exist. JacquesLucke: This seems to be outdated, the `BLI_color_convert_to_linear` function does not exist. | |||||
| * ``` | |||||
| * | |||||
| * The API is structured to make most use of inlining. Most notably is that space | |||||
| * conversions must be done via `BLI_color_convert_to*` functions. | |||||
| * | |||||
| * - Conversions between spaces (srgb <=> scene linear) should always be done by | |||||
| * invoking the `BLI_color_convert_to*` methods. | |||||
| * - Encoding colors (compressing to store colors inside a less precision storage) | |||||
| * should be done by invoking the `encode` and `decode` methods. | |||||
| * - Changing alpha association should be done by invoking `to_multiplied_alpha` or | |||||
Done Inline Actionstypo (to_(pre)multiplied_alpha) JacquesLucke: typo (`to_(pre)multiplied_alpha`) | |||||
| * `to_straight_alpha` methods. | |||||
| * | |||||
| * # Encoding. | |||||
| * | |||||
| * Color encoding is used to store colors with less precision using uint8_t in | |||||
| * stead of floats. This encoding is supported for the `eSpace::SceneLinear`. | |||||
| * To make this clear to the developer the a `eSpace::SceneLinearByteEncoded` | |||||
Done Inline Actionstypo (a) JacquesLucke: typo (`a`) | |||||
| * space is added. | |||||
| * | |||||
| * # sRGB precision | |||||
| * | |||||
| * The sRGB colors can be stored using `uint8_t` or `float` colors. The conversion | |||||
| * between the two precisions are available as methods. (`to_srgb4b` and | |||||
| * `to_srgb4f`). | |||||
| * | |||||
Done Inline ActionsThis should not be hardcoded, it's not correct if a different OpenColorIO config than the default is used. When dealing with vertex colors as in this patch, I'd use two color spaces:
Not sure about the best names, it could be "byte", "compressed", something along those lines. But we should not consider vertex colors to be in sRGB space or for there to be some fixed "linear" space. brecht: This should not be hardcoded, it's not correct if a different OpenColorIO config than the… | |||||
Done Inline ActionsThat is a good one. In the gpu/draw module it is also confusing when we mean sRGB (themes) and when it is linear, but with byte encoding. What happens to be implemented as srgb. I prefer the term SceneLinearByteEncoded. A bit long, but gives clarity. Would this then also be the time to link this to actual ocio config. I do see some challenges there and would suggest to do it as a separate project after this one is completed due to the tight relation with srgb buffers and different goals (code deconfussion vs more flexible color pipeline). jbakker: That is a good one. In the gpu/draw module it is also confusing when we mean sRGB (themes) and… | |||||
Done Inline ActionsByteEncoded sounds good to me. I'm not sure what linking to the OCIO config means exactly, but I think just naming the colors after the roles they correspond to in the OCIO config is most important. brecht: `ByteEncoded` sounds good to me.
I'm not sure what linking to the OCIO config means exactly… | |||||
| * # Alpha conversion | |||||
| * | |||||
| * Alpha conversion is only supported in SceneLinear space. | |||||
| * | |||||
| * Extending this file: | |||||
| * - This file can be extended with `ColorHex/Hsl/Hsv` for different representations | |||||
| * of rgb based colors. | |||||
| * - Add ColorXyz. | |||||
| */ | |||||
| /* Enumeration containing the different alpha modes. */ | |||||
| enum class eAlpha { | |||||
| /* Color and alpha are unassociated. */ | |||||
| Straight, | |||||
| /* Color and alpha are associated. */ | |||||
| Premultiplied, | |||||
Done Inline ActionsThe reference color space in OpenColorIO configs is not relevant to how Blender works. It's important for the config to define what the transforms are relative to, but there are no colors in Blender in this reference space. I guess this is meant to be SceneLinear instead. brecht: The reference color space in OpenColorIO configs is not relevant to how Blender works. It's… | |||||
| }; | |||||
| std::ostream &operator<<(std::ostream &stream, const eAlpha &space); | |||||
| /* Enumeration containing internal spaces. */ | |||||
| enum class eSpace { | |||||
| /* sRGB color space. */ | |||||
| Srgb, | |||||
brechtUnsubmitted Done Inline ActionsI would not add an sRGB color space when the implementation is known to not take into account what the scene linear color space is. Any code using this may introduce bugs, so I would recommend having it. brecht: I would not add an sRGB color space when the implementation is known to not take into account… | |||||
jbakkerAuthorUnsubmitted Done Inline ActionsI am a bit between two solutions here. Theme colors are in sRGB. when removing sRGB and not changing the current logic users would do the conversion the old way that is error prone. char theme_color[4]; float linear_color[4]; srgb_to_linearrgb_v4(linear_color, srgb_color) ColorSceneLinear4f<eAlpha::Straight> scene_linear(linear_color) A future proof solution would be to use OCIO for the conversion between known color spaces and roles. I think you're right to remove the confusion from start and only add it when we have connected it to color management. jbakker: I am a bit between two solutions here. Theme colors are in sRGB. when removing sRGB and not… | |||||
brechtUnsubmitted Done Inline ActionsI think it's fine to have a color space for theme colors. And a case I forgot is the usage of theme colors in the viewport background, which needs a conversion to scene linear. Maybe we can rename ColorSrgb to ColorTheme or ColorUI? brecht: I think it's fine to have a color space for theme colors. And a case I forgot is the usage of… | |||||
| /* Blender internal scene linear color space (maps to SceneReference role in OCIO). */ | |||||
| SceneLinear, | |||||
| /* Blender internal scene linear color space compressed to be stored in 4 uint8_t. */ | |||||
| SceneLinearByteEncoded, | |||||
| }; | |||||
| std::ostream &operator<<(std::ostream &stream, const eSpace &space); | |||||
| Color4f() = default; | /* Template class to store RGBA values with different precision, space and alpha association. */ | ||||
| template<typename ChannelStorageType, eSpace Space, eAlpha Alpha> class ColorRGBA { | |||||
| public: | |||||
| ChannelStorageType r, g, b, a; | |||||
| constexpr ColorRGBA() = default; | |||||
| Color4f(const float *rgba) : r(rgba[0]), g(rgba[1]), b(rgba[2]), a(rgba[3]) | constexpr ColorRGBA(const ChannelStorageType rgba[4]) | ||||
| : r(rgba[0]), g(rgba[1]), b(rgba[2]), a(rgba[3]) | |||||
| { | { | ||||
| } | } | ||||
| Color4f(float r, float g, float b, float a) : r(r), g(g), b(b), a(a) | constexpr ColorRGBA(const ChannelStorageType r, | ||||
Done Inline ActionsI think this name should be a bit more specific, like convert_color_space. Also it might make sense to take src as a reference, just for consistency. JacquesLucke: I think this name should be a bit more specific, like `convert_color_space`. Also it might make… | |||||
| const ChannelStorageType g, | |||||
| const ChannelStorageType b, | |||||
| const ChannelStorageType a) | |||||
| : r(r), g(g), b(b), a(a) | |||||
| { | { | ||||
| } | } | ||||
| operator float *() | operator ChannelStorageType *() | ||||
| { | { | ||||
| return &r; | return &r; | ||||
| } | } | ||||
| operator const float *() const | operator const ChannelStorageType *() const | ||||
| { | { | ||||
| return &r; | return &r; | ||||
| } | } | ||||
| friend std::ostream &operator<<(std::ostream &stream, Color4f c) | friend std::ostream &operator<<(std::ostream &stream, | ||||
Done Inline ActionsI don't think this kind of automatic conversion in a constructor is good. These conversions can be lossy. For example you would not want a function that has a temporary variable with a different alpha mode than the data it is operating on, because you will lose data. What I think is helpful is communicating intent and type safety. I'd rather have specific functions that associate/unassociate alpha, decode/encode byte colors, etc. brecht: I don't think this kind of automatic conversion in a constructor is good.
These conversions… | |||||
Done Inline ActionsAlthough I tried to do no conversion in constructor at all, it wasn't working for me. The compiler wasn't smart enough to select the correct template function. Hence it is commented out. When using specific conversion methods introduces more bugs as you really need to specify the correct constructor. When this isn't done the compiler will select float* or uint8_t* constructor what isn't expected by the developer. See SrgbStraightToSceneLinearPremultiplied for example where every step needs to be specified. We could simplify this for common transformations, but I think the current setup is too error-prone. Another solution would be to make the color space + storage as a single class with concrete methods eg Srgb4b/SceneLinear4f as subclasses from abstract classes Color4b and Color4f jbakker: Although I tried to do no conversion in constructor at all, it wasn't working for me. The… | |||||
Done Inline ActionsI think it's fine to do no color space conversion in constructors and only use standalone functions with descriptive names instead. That should make things more explicit indeed. I can see the issue with the float/uint8_t * constructor. The issue here is not really the constructor though but the implicit conversion to a pointer. This was causing issues for float3 as well sometimes. The best solution might be to just remove operator float *() and similar conversion operators. Instead people could use &color.r or something like color.ptr(). It's not pretty but at least it does not have these issues. JacquesLucke: I think it's fine to do no color space conversion in constructors and only use standalone… | |||||
| const ColorRGBA<ChannelStorageType, Space, Alpha> &c) | |||||
| { | { | ||||
| stream << "(" << c.r << ", " << c.g << ", " << c.b << ", " << c.a << ")"; | |||||
| stream << Space << Alpha << "(" << c.r << ", " << c.g << ", " << c.b << ", " << c.a << ")"; | |||||
| return stream; | return stream; | ||||
| } | } | ||||
| friend bool operator==(const Color4f &a, const Color4f &b) | friend bool operator==(const ColorRGBA<ChannelStorageType, Space, Alpha> &a, | ||||
| const ColorRGBA<ChannelStorageType, Space, Alpha> &b) | |||||
| { | { | ||||
| return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a; | return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a; | ||||
| } | } | ||||
| friend bool operator!=(const Color4f &a, const Color4f &b) | friend bool operator!=(const ColorRGBA<ChannelStorageType, Space, Alpha> &a, | ||||
| const ColorRGBA<ChannelStorageType, Space, Alpha> &b) | |||||
| { | { | ||||
| return !(a == b); | return !(a == b); | ||||
| } | } | ||||
| uint64_t hash() const | uint64_t hash() const | ||||
| { | { | ||||
| uint64_t x1 = *reinterpret_cast<const uint32_t *>(&r); | uint64_t x1 = *reinterpret_cast<const uint32_t *>(&r); | ||||
| uint64_t x2 = *reinterpret_cast<const uint32_t *>(&g); | uint64_t x2 = *reinterpret_cast<const uint32_t *>(&g); | ||||
| uint64_t x3 = *reinterpret_cast<const uint32_t *>(&b); | uint64_t x3 = *reinterpret_cast<const uint32_t *>(&b); | ||||
| uint64_t x4 = *reinterpret_cast<const uint32_t *>(&a); | uint64_t x4 = *reinterpret_cast<const uint32_t *>(&a); | ||||
| return (x1 * 1283591) ^ (x2 * 850177) ^ (x3 * 735391) ^ (x4 * 442319); | return (x1 * 1283591) ^ (x2 * 850177) ^ (x3 * 735391) ^ (x4 * 442319); | ||||
| } | } | ||||
| }; | }; | ||||
| struct Color4b { | /* Forward declarations of concrete color classes. */ | ||||
| uint8_t r, g, b, a; | template<eAlpha Alpha> class ColorSceneLinear4f; | ||||
| template<eAlpha Alpha> class ColorSceneLinearByteEncoded4b; | |||||
| template<typename ChannelStorageType> class ColorSrgb4; | |||||
| Color4b() = default; | /* Forward declation of precision conversion methods. */ | ||||
| BLI_INLINE ColorSrgb4<float> BLI_color_convert_to_srgb4f(const ColorSrgb4<uint8_t> &srgb4b); | |||||
| BLI_INLINE ColorSrgb4<uint8_t> BLI_color_convert_to_srgb4b(const ColorSrgb4<float> &srgb4f); | |||||
| Color4b(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : r(r), g(g), b(b), a(a) | template<eAlpha Alpha> | ||||
| class ColorSceneLinear4f final : public ColorRGBA<float, eSpace::SceneLinear, Alpha> { | |||||
| public: | |||||
| constexpr ColorSceneLinear4f<Alpha>() : ColorRGBA<float, eSpace::SceneLinear, Alpha>() | |||||
| { | { | ||||
| } | } | ||||
| Color4b(Color4f other) | constexpr ColorSceneLinear4f<Alpha>(const float *rgba) | ||||
| : ColorRGBA<float, eSpace::SceneLinear, Alpha>(rgba) | |||||
| { | { | ||||
| rgba_float_to_uchar(*this, other); | |||||
| } | } | ||||
| operator Color4f() const | constexpr ColorSceneLinear4f<Alpha>(float r, float g, float b, float a) | ||||
| : ColorRGBA<float, eSpace::SceneLinear, Alpha>(r, g, b, a) | |||||
| { | { | ||||
| Color4f result; | |||||
| rgba_uchar_to_float(result, *this); | |||||
| return result; | |||||
| } | } | ||||
| operator uint8_t *() | /** | ||||
| * Convert to its byte encoded counter space. | |||||
| **/ | |||||
| ColorSceneLinearByteEncoded4b<Alpha> to_byte_encoded() const | |||||
| { | { | ||||
| return &r; | ColorSceneLinearByteEncoded4b<Alpha> encoded; | ||||
| linearrgb_to_srgb_uchar4(encoded, *this); | |||||
| return encoded; | |||||
| } | } | ||||
| operator const uint8_t *() const | /** | ||||
| * Convert color and alpha association to premultiplied alpha. | |||||
| * | |||||
| * Will assert when called on a color premultiplied with alpha. | |||||
| */ | |||||
| ColorSceneLinear4f<eAlpha::Premultiplied> to_premultiplied_alpha() const | |||||
Done Inline ActionsIt feels like either (1) this method should not exist when when Alpha != Straight or (2) it should just return the value unchanged in this case.
The same applies to the method below of course. JacquesLucke: It feels like either (1) this method should not exist when when `Alpha != Straight` or (2) it… | |||||
| { | { | ||||
| return &r; | BLI_assert(Alpha == eAlpha::Straight); | ||||
| ColorSceneLinear4f<eAlpha::Premultiplied> premultiplied; | |||||
| straight_to_premul_v4_v4(premultiplied, *this); | |||||
| return premultiplied; | |||||
| } | } | ||||
| friend std::ostream &operator<<(std::ostream &stream, Color4b c) | /** | ||||
| * Convert color and alpha association to straight alpha. | |||||
| * | |||||
| * Will assert when called on a color with straight alpha.. | |||||
Done Inline Actionstypo (..) JacquesLucke: typo (`..`) | |||||
| */ | |||||
| ColorSceneLinear4f<eAlpha::Straight> to_straight_alpha() const | |||||
| { | |||||
| BLI_assert(Alpha == eAlpha::Premultiplied); | |||||
| ColorSceneLinear4f<eAlpha::Straight> straighten; | |||||
| premul_to_straight_v4_v4(straighten, *this); | |||||
| return straighten; | |||||
| } | |||||
| }; | |||||
| template<eAlpha Alpha> | |||||
| class ColorSceneLinearByteEncoded4b final | |||||
| : public ColorRGBA<uint8_t, eSpace::SceneLinearByteEncoded, Alpha> { | |||||
| public: | |||||
| constexpr ColorSceneLinearByteEncoded4b() = default; | |||||
| constexpr ColorSceneLinearByteEncoded4b(const float *rgba) | |||||
Done Inline ActionsShould this constructor and the one below take uint8_ts as argument? JacquesLucke: Should this constructor and the one below take `uint8_t`s as argument? | |||||
| : ColorRGBA<uint8_t, eSpace::SceneLinearByteEncoded, Alpha>(rgba) | |||||
| { | { | ||||
| stream << "(" << c.r << ", " << c.g << ", " << c.b << ", " << c.a << ")"; | |||||
| return stream; | |||||
| } | } | ||||
| friend bool operator==(const Color4b &a, const Color4b &b) | constexpr ColorSceneLinearByteEncoded4b(float r, float g, float b, float a) | ||||
| : ColorRGBA<uint8_t, eSpace::SceneLinearByteEncoded, Alpha>(r, g, b, a) | |||||
| { | { | ||||
| return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a; | |||||
| } | } | ||||
| friend bool operator!=(const Color4b &a, const Color4b &b) | /** | ||||
| * Convert to back to float color. | |||||
| **/ | |||||
Done Inline Actionstypo (**) JacquesLucke: typo (`**`) | |||||
| ColorSceneLinear4f<Alpha> to_byte_decoded() const | |||||
| { | { | ||||
| return !(a == b); | ColorSceneLinear4f<Alpha> decoded; | ||||
| srgb_to_linearrgb_uchar4(decoded, *this); | |||||
| return decoded; | |||||
| } | } | ||||
| }; | |||||
| uint64_t hash() const | /** | ||||
| * Srgb color template class. Should not be used directly. When needed please use | |||||
| * the convenience `ColorSrgb4b` and `ColorSrgb4f` declarations. | |||||
| */ | |||||
| template<typename ChannelStorageType> | |||||
| class ColorSrgb4 final : public ColorRGBA<ChannelStorageType, eSpace::Srgb, eAlpha::Straight> { | |||||
| public: | |||||
| constexpr ColorSrgb4() : ColorRGBA<ChannelStorageType, eSpace::Srgb, eAlpha::Straight>() | |||||
| { | { | ||||
| return static_cast<uint64_t>(r * 1283591) ^ static_cast<uint64_t>(g * 850177) ^ | } | ||||
| static_cast<uint64_t>(b * 735391) ^ static_cast<uint64_t>(a * 442319); | |||||
| constexpr ColorSrgb4(const ChannelStorageType *rgba) | |||||
| : ColorRGBA<ChannelStorageType, eSpace::Srgb, eAlpha::Straight>(rgba) | |||||
| { | |||||
| } | |||||
| constexpr ColorSrgb4(ChannelStorageType r, | |||||
| ChannelStorageType g, | |||||
| ChannelStorageType b, | |||||
| ChannelStorageType a) | |||||
| : ColorRGBA<ChannelStorageType, eSpace::Srgb, eAlpha::Straight>(r, g, b, a) | |||||
| { | |||||
| } | |||||
| /** | |||||
| * Change precision of color to float. | |||||
| * | |||||
| * Will fail to compile when invoked on a float color. | |||||
| */ | |||||
| ColorSrgb4<float> to_srgb4f() const | |||||
| { | |||||
| BLI_assert(typeof(r) == uint8_t); | |||||
| return BLI_color_convert_to_srgb4f(*this); | |||||
| } | |||||
| /** | |||||
| * Change precision of color to uint8_t. | |||||
| * | |||||
| * Will fail to compile when invoked on a uint8_t color. | |||||
| */ | |||||
| ColorSrgb4<uint8_t> to_srgb4b() const | |||||
| { | |||||
| BLI_assert(typeof(r) == float); | |||||
| return BLI_color_convert_to_srgb4b(*this); | |||||
| } | } | ||||
| }; | }; | ||||
| using ColorSrgb4f = ColorSrgb4<float>; | |||||
| using ColorSrgb4b = ColorSrgb4<uint8_t>; | |||||
| BLI_INLINE ColorSrgb4b BLI_color_convert_to_srgb4b(const ColorSrgb4f &srgb4f) | |||||
| { | |||||
| ColorSrgb4b srgb4b; | |||||
| rgba_float_to_uchar(srgb4b, srgb4f); | |||||
| return srgb4b; | |||||
Done Inline ActionsNot sure I understand that last sentence. JacquesLucke: Not sure I understand that last sentence. | |||||
| } | |||||
| BLI_INLINE ColorSrgb4f BLI_color_convert_to_srgb4f(const ColorSrgb4b &srgb4b) | |||||
| { | |||||
| ColorSrgb4f srgb4f; | |||||
| rgba_uchar_to_float(srgb4f, srgb4b); | |||||
| return srgb4f; | |||||
| } | |||||
| BLI_INLINE ColorSceneLinear4f<eAlpha::Straight> BLI_color_convert_to_scene_linear( | |||||
| const ColorSrgb4f &srgb4f) | |||||
| { | |||||
| ColorSceneLinear4f<eAlpha::Straight> scene_linear; | |||||
| srgb_to_linearrgb_v4(scene_linear, srgb4f); | |||||
| return scene_linear; | |||||
| } | |||||
| BLI_INLINE ColorSceneLinear4f<eAlpha::Straight> BLI_color_convert_to_scene_linear( | |||||
| const ColorSrgb4b &srgb4b) | |||||
| { | |||||
| ColorSceneLinear4f<eAlpha::Straight> scene_linear; | |||||
| srgb_to_linearrgb_uchar4(scene_linear, srgb4b); | |||||
| return scene_linear; | |||||
| } | |||||
| BLI_INLINE ColorSrgb4f | |||||
| BLI_color_convert_to_srgb4f(const ColorSceneLinear4f<eAlpha::Straight> &scene_linear) | |||||
Done Inline ActionsThis doesn't compile for me. JacquesLucke: This doesn't compile for me.
Use `BLI_assert((std::is_same_v<ChannelStorageType, uint8_t>));`… | |||||
| { | |||||
| ColorSrgb4f srgb4f; | |||||
| linearrgb_to_srgb_v4(srgb4f, scene_linear); | |||||
| return srgb4f; | |||||
| } | |||||
| BLI_INLINE ColorSrgb4b | |||||
| BLI_color_convert_to_srgb4b(const ColorSceneLinear4f<eAlpha::Straight> &scene_linear) | |||||
| { | |||||
| ColorSrgb4b srgb4b; | |||||
| linearrgb_to_srgb_uchar4(srgb4b, scene_linear); | |||||
| return srgb4b; | |||||
| } | |||||
| /* Internal roles. For convenience to shorten the type names and hide complexity | |||||
| * in areas where transformations are unlikely to happen. */ | |||||
| using ColorSceneReference4f = ColorSceneLinear4f<eAlpha::Premultiplied>; | |||||
brechtUnsubmitted Done Inline ActionsI would not use "scene reference" terminology. "Reference" has a specific meaning in OpenColorIO, and it's different. Maybe giving the ColorSceneLinear4f template a default value of eAlpha::Premultiplied works? Then we don't have to introduce a new term. brecht: I would not use "scene reference" terminology. "Reference" has a specific meaning in… | |||||
jbakkerAuthorUnsubmitted Done Inline ActionsBoth premultiplied and straight is used in blender (compositor and selected operations). using ColorGeometry4f = ColorSceneLinear4f<eAlpha::Premultiplied>; using ColorGeometry4b = ColorSceneLinearByteEncoded4b<eAlpha::Premultiplied>; jbakker: Both premultiplied and straight is used in blender (compositor and selected operations).
My… | |||||
brechtUnsubmitted Done Inline ActionsFine with me too. brecht: Fine with me too. | |||||
| using ColorSceneReference4b = ColorSceneLinearByteEncoded4b<eAlpha::Premultiplied>; | |||||
| using ColorTheme4b = ColorSrgb4b; | |||||
| using ColorGeometry4f = ColorSceneReference4f; | |||||
| using ColorGeometry4b = ColorSceneLinearByteEncoded4b<eAlpha::Premultiplied>; | |||||
| } // namespace blender | } // namespace blender | ||||
Done Inline Actionsnew line jbakker: new line | |||||
typo (typically)