A while back, I took a break from responsibilities and played around with generating 10Print and 10Print-like diagrams. For those not in the know, check out the website 10Print.org!
I did my 10Print renders using my own creative coding tool (which may be a future blog post), but today I’ll create the renders in the SVG format.
We’ll be using Rust, the svg
crate to generate SVGs, and the rand
crate for random number generation.
Variants
Let’s take a look at 10Print and some variants.
Original
- Consider the canvas as a grid of cells.
-
For each cell in the canvas, randomly draw a line which is either:
- From the top left to the bottom right
- From the bottom left to the top right
use rand::Rng;
use svg::{node::element::{path::Data, Path}, Node};
/// Draw a line from start (x1, y1) to end (x2, y2).
fn (: (, ), : (, )) -> Path {
Path::new().set("d", Data::new().move_to().line_to().close())
}
fn () -> <(), <dyn std::error::>> {
// 10Print parameters
let = 1000;
let = 500;
let = 20;
let = / ;
let = "output.svg";
// Create the svg document
let = (0, 0, , );
let mut = svg::Document::new().set("viewBox", );
let mut = rand::rngs::StdRng::seed_from_u64(8888);
for in (0..).( as ) {
for in (0..).( as ) {
// Half the time...
let = if .gen::<>() > 0.5 {
// Draw a top left -> bottom right line
((, ), ( + , + ));
} else {
// Draw a bottom left -> top right line
((, + ), ( + , ));
};
.append();
}
}
svg::save(, &)?;
(())
}
Output:
Pretty neat! With the two simple rules, we get an aesthetically pleasing tiled image. We can also bias the lines towards one diagonal or another by modifying the weight in the random number check:
Top-left to bottom right bias:
// Most of the time...
let line = if rng.gen::<>() > 0.2 {
// Draw a top left -> bottom right line
((x, y), (x + spacing, y + spacing))
} else {
Bottom-left to top-right bias:
// Very rarely...
let line = if rng.gen::<>() > 0.8 {
// Draw a top left -> bottom right line
((x, y), (x + spacing, y + spacing))
} else {
Orthogonal
What if, instead of diagonal lines, we drew straight lines instead?
let line = if rng.gen::<>() > 0.5 {
((x, y), (x + spacing, y))
} else {
((x, y), (x, y + spacing))
};
Pretty cool! It resembles a very biased and unfun maze.
Weave
This variant is similar to the Orthogonal variant, but with a different offset:
let line = if rng.gen::<>() > 0.5 {
((x + spacing / 2, y), (x + spacing / 2, y + spacing))
} else {
((x, y + spacing / 2), (x + spacing, y + spacing / 2))
};
Kinda looks like a basket weave, don’t you think?
Mondrian-ish
How about we connect the lines in the Weave variant?
let line = if rng.gen::<>() > 0.5 {
(
(x + spacing / 2, y - spacing / 2),
(x + spacing / 2, y + spacing * 3 / 2),
)
} else {
(
(x - spacing / 2, y + spacing / 2),
(x + spacing * 3 / 2, y + spacing / 2),
)
};
Budget Piet Mondrian art!
Bark
There’s no restriction on the number of separate “cases” we create, either. Here’s a version with 3 cases instead of two:
let rand_num = rng.gen::<>();
let line = if rand_num < 0.1 {
((x, y), (x + spacing, y + spacing))
} else if rand_num < 0.4 {
((x, y + spacing), (x + spacing, y))
} else {
((x + spacing, y), (x + spacing, y + spacing))
};
Color
Black and white images are too boring, so let’s inject some color into the renders.
First, we’ll make the line
function accept a color:
fn (: (, ), : (, ), : &) -> Path {
Path::new()
.set("stroke", )
.set("d", Data::new().move_to().line_to().close())
}
Then, we modify the calls to draw either a red line or a blue line:
let line = if rand_num > 0.5 {
// Draw a top left -> bottom right line
((x, y), (x + spacing, y + spacing), "red");
} else {
// Draw a bottom left -> top right line
((x, y + spacing), (x + spacing, y), "blue");
}
More 10Print Variants
Here are a few more 10Print variants. Try to create them yourself or be creative and make your own designs!
Note: This one is a recreation of Ian Witham’s 10Print variation.
let width: i32
i32
The 32-bit signed integer type.
usize
The pointer-sized unsigned integer type.
The size of this primitive is how many bytes it takes to reference any location in memory. For example, on a 32 bit target, this is 4 bytes and on a 64 bit target, this is 8 bytes.
alloc::boxed
pub struct Box<T, A = Global>(Unique<T>, A)
where
T: ?Sized,
A: Allocator,
A pointer type that uniquely owns a heap allocation of type T
.
See the module-level documentation for more.
let mut rng: {unknown}
core::macros::builtin
macro_rules! line
Expands to the line number on which it was invoked.
With column
and file
, these macros provide debugging information for
developers about the location within the source.
The expanded expression has type u32
and is 1-based, so the first line
in each file evaluates to 1, the second to 2, etc. This is consistent
with error messages by common compilers or popular editors.
The returned line is not necessarily the line of the line!
invocation itself,
but rather the first macro invocation leading up to the invocation
of the line!
macro.
Examples
let current_line = line!();
println!("defined on line: {current_line}");
let height: i32
color: &str
src
fn line(start: (i32, i32), end: (i32, i32), color: &str) -> Path
core::result
pub enum Result<T, E> {
Ok( /* … */ ),
Err( /* … */ ),
}
Result
is a type that represents either success (Ok
) or failure (Err
).
See the documentation for details.
end: (i32, i32)
core::iter::traits::iterator::Iterator
pub trait Iterator
pub fn step_by(self, step: usize) -> StepBy<Self>
where
Self: Sized,
Creates an iterator starting at the same point, but stepping by the given amount at each iteration.
Note 1: The first element of the iterator will always be returned, regardless of the step given.
Note 2: The time at which ignored elements are pulled is not fixed.
StepBy
behaves like the sequence self.next()
, self.nth(step-1)
,
self.nth(step-1)
, …, but is also free to behave like the sequence
advance_n_and_return_first(&mut self, step)
,
advance_n_and_return_first(&mut self, step)
, …
Which way is used may change for some iterators for performance reasons.
The second way will advance the iterator earlier and may consume more items.
advance_n_and_return_first
is the equivalent of:
fn advance_n_and_return_first<I>(iter: &mut I, n: usize) -> Option<I::Item>
where
I: Iterator,
{
let next = iter.next();
if n > 1 {
iter.nth(n - 2);
}
next
}
Panics
The method will panic if the given step is 0
.
Examples
let a = [0, 1, 2, 3, 4, 5];
let mut iter = a.iter().step_by(2);
assert_eq!(iter.next(), Some(&0));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&4));
assert_eq!(iter.next(), None);
core::result::Result
Ok(T)
Contains the success value
src
fn line(start: (i32, i32), end: (i32, i32)) -> Path
Draw a line from start (x1, y1) to end (x2, y2).
let viewbox: (i32, i32, i32, i32)
start: (i32, i32)
src
fn main() -> Result<(), Box<dyn std::error::Error>>
let x: i32
let line: ()
let num_cells: i32
let spacing: i32
f32
A 32-bit floating-point type (specifically, the “binary32” type defined in IEEE 754-2008).
This type can represent a wide range of decimal numbers, like 3.5
, 27
,
-113.75
, 0.0078125
, 34359738368
, 0
, -1
. So unlike integer types
(such as i32
), floating-point types can represent non-integer numbers,
too.
However, being able to represent this wide range of numbers comes at the
cost of precision: floats can only represent some of the real numbers and
calculation with floats round to a nearby representable number. For example,
5.0
and 1.0
can be exactly represented as f32
, but 1.0 / 5.0
results
in 0.20000000298023223876953125
since 0.2
cannot be exactly represented
as f32
. Note, however, that printing floats with println
and friends will
often discard insignificant digits: println!("{}", 1.0f32 / 5.0f32)
will
print 0.2
.
Additionally, f32
can represent some special values:
- −0.0: IEEE 754 floating-point numbers have a bit that indicates their sign, so −0.0 is a possible value. For comparison −0.0 = +0.0, but floating-point operations can carry the sign bit through arithmetic operations. This means −0.0 × +0.0 produces −0.0 and a negative number rounded to a value smaller than a float can represent also produces −0.0.
- ∞ and
−∞: these result from calculations
like
1.0 / 0.0
. - NaN (not a number): this value results from
calculations like
(-1.0).sqrt()
. NaN has some potentially unexpected behavior:- It is not equal to any float, including itself! This is the reason
f32
doesn’t implement theEq
trait. - It is also neither smaller nor greater than any float, making it
impossible to sort by the default comparison operation, which is the
reason
f32
doesn’t implement theOrd
trait. - It is also considered infectious as almost all calculations where one of the operands is NaN will also result in NaN. The explanations on this page only explicitly document behavior on NaN operands if this default is deviated from.
- Lastly, there are multiple bit patterns that are considered NaN.
Rust does not currently guarantee that the bit patterns of NaN are
preserved over arithmetic operations, and they are not guaranteed to be
portable or even fully deterministic! This means that there may be some
surprising results upon inspecting the bit patterns,
as the same calculations might produce NaNs with different bit patterns.
This also affects the sign of the NaN: checking
is_sign_positive
oris_sign_negative
on a NaN is the most common way to run into these surprising results. (Checkingx >= 0.0
orx <= 0.0
avoids those surprises, but also how negative/positive zero are treated.) See the section below for what exactly is guaranteed about the bit pattern of a NaN.
- It is not equal to any float, including itself! This is the reason
When a primitive operation (addition, subtraction, multiplication, or division) is performed on this type, the result is rounded according to the roundTiesToEven direction defined in IEEE 754-2008. That means:
- The result is the representable value closest to the true value, if there is a unique closest representable value.
- If the true value is exactly half-way between two representable values, the result is the one with an even least-significant binary digit.
- If the true value’s magnitude is ≥
f32::MAX
+ 2(f32::MAX_EXP
−f32::MANTISSA_DIGITS
− 1), the result is ∞ or −∞ (preserving the true value’s sign). - If the result of a sum exactly equals zero, the outcome is +0.0 unless
both arguments were negative, then it is -0.0. Subtraction
a - b
is regarded as a suma + (-b)
.
For more information on floating-point numbers, see Wikipedia.
See also the std::f32::consts
module.
NaN bit patterns
This section defines the possible NaN bit patterns returned by floating-point operations.
The bit pattern of a floating-point NaN value is defined by:
- a sign bit.
- a quiet/signaling bit. Rust assumes that the quiet/signaling bit being set to
1
indicates a quiet NaN (QNaN), and a value of0
indicates a signaling NaN (SNaN). In the following we will hence just call it the “quiet bit”. - a payload, which makes up the rest of the significand (i.e., the mantissa) except for the quiet bit.
The rules for NaN values differ between arithmetic and non-arithmetic (or “bitwise”)
operations. The non-arithmetic operations are unary -
, abs
, copysign
, signum
,
{to,from}_bits
, {to,from}_{be,le,ne}_bytes
and is_sign_{positive,negative}
. These
operations are guaranteed to exactly preserve the bit pattern of their input except for possibly
changing the sign bit.
The following rules apply when a NaN value is returned from an arithmetic operation:
-
The result has a non-deterministic sign.
-
The quiet bit and payload are non-deterministically chosen from the following set of options:
- Preferred NaN: The quiet bit is set and the payload is all-zero.
- Quieting NaN propagation: The quiet bit is set and the payload is copied from any input
operand that is a NaN. If the inputs and outputs do not have the same payload size (i.e., for
as
casts), then- If the output is smaller than the input, low-order bits of the payload get dropped.
- If the output is larger than the input, the payload gets filled up with 0s in the low-order bits.
- Unchanged NaN propagation: The quiet bit and payload are copied from any input operand
that is a NaN. If the inputs and outputs do not have the same size (i.e., for
as
casts), the same rules as for “quieting NaN propagation” apply, with one caveat: if the output is smaller than the input, droppig the low-order bits may result in a payload of 0; a payload of 0 is not possible with a signaling NaN (the all-0 significand encodes an infinity) so unchanged NaN propagation cannot occur with some inputs. - Target-specific NaN: The quiet bit is set and the payload is picked from a target-specific set of “extra” possible NaN payloads. The set can depend on the input operand values. See the table below for the concrete NaNs this set contains on various targets.
In particular, if all input NaNs are quiet (or if there are no input NaNs), then the output NaN is definitely quiet. Signaling NaN outputs can only occur if they are provided as an input value. Similarly, if all input NaNs are preferred (or if there are no input NaNs) and the target does not have any “extra” NaN payloads, then the output NaN is guaranteed to be preferred.
The non-deterministic choice happens when the operation is executed; i.e., the result of a NaN-producing floating-point operation is a stable bit pattern (looking at these bits multiple times will yield consistent results), but running the same operation twice with the same inputs can produce different results.
These guarantees are neither stronger nor weaker than those of IEEE 754: IEEE 754 guarantees
that an operation never returns a signaling NaN, whereas it is possible for operations like
SNAN * 1.0
to return a signaling NaN in Rust. Conversely, IEEE 754 makes no statement at all
about which quiet NaN is returned, whereas Rust restricts the set of possible results to the
ones listed above.
Unless noted otherwise, the same rules also apply to NaNs returned by other library functions
(e.g. min
, minimum
, max
, maximum
); other aspects of their semantics and which IEEE 754
operation they correspond to are documented with the respective functions.
When an arithmetic floating-point operation is executed in const
context, the same rules
apply: no guarantee is made about which of the NaN bit patterns described above will be
returned. The result does not have to match what happens when executing the same code at
runtime, and the result can vary depending on factors such as compiler version and flags.
Target-specific “extra” NaN values
target_arch |
Extra payloads possible on this platform |
---|---|
x86 , x86_64 , arm , aarch64 , riscv32 , riscv64 |
None |
sparc , sparc64 |
The all-one payload |
wasm32 , wasm64 |
If all input NaNs are quiet with all-zero payload: None. Otherwise: all possible payloads. |
For targets not in this table, all payloads are possible.
let mut document: {unknown}
let output_file: &str
let y: i32
core::error
// Dyn Compatible: Yes
pub trait Error
where
Self: Debug + Display,
Error
is a trait representing the basic expectations for error values,
i.e., values of type E
in Result<T, E>
.
Errors must describe themselves through the Display
and Debug
traits. Error messages are typically concise lowercase sentences without
trailing punctuation:
let err = "NaN".parse::<u32>().unwrap_err();
assert_eq!(err.to_string(), "invalid digit found in string");
Errors may provide cause information. Error::source
is generally
used when errors cross “abstraction boundaries”. If one module must report
an error that is caused by an error from a lower-level module, it can allow
accessing that error via Error::source
. This makes it possible for the
high-level module to provide its own errors while also revealing some of the
implementation for debugging.
str
String slices.
The str
type, also called a ‘string slice’, is the most primitive string
type. It is usually seen in its borrowed form, &str
. It is also the type
of string literals, &'static str
.
Basic Usage
String literals are string slices:
let hello_world = "Hello, World!";
Here we have declared a string slice initialized with a string literal.
String literals have a static lifetime, which means the string hello_world
is guaranteed to be valid for the duration of the entire program.
We can explicitly specify hello_world
’s lifetime as well:
let hello_world: &'static str = "Hello, world!";
Representation
A &str
is made up of two components: a pointer to some bytes, and a
length. You can look at these with the [as_ptr
] and [len
] methods:
use std::slice;
use std::str;
let story = "Once upon a time...";
let ptr = story.as_ptr();
let len = story.len();
// story has nineteen bytes
assert_eq!(19, len);
// We can re-build a str out of ptr and len. This is all unsafe because
// we are responsible for making sure the two components are valid:
let s = unsafe {
// First, we build a &[u8]...
let slice = slice::from_raw_parts(ptr, len);
// ... and then convert that slice into a string slice
str::from_utf8(slice)
};
assert_eq!(s, Ok(story));
Note: This example shows the internals of &str
. unsafe
should not be
used to get a string slice under normal circumstances. Use as_str
instead.
Invariant
Rust libraries may assume that string slices are always valid UTF-8.
Constructing a non-UTF-8 string slice is not immediate undefined behavior, but any function called on a string slice may assume that it is valid UTF-8, which means that a non-UTF-8 string slice can lead to undefined behavior down the road.