In [1]:
import brushcue
ctx = brushcue.Context()
HOME = "/home/dito/dev/monorepo/writing/graphics/chapters/curves/assets/cardinal-cubic"
POINTS = [
(-3.0, 0.0),
(-2.0, 0.0),
(-1.0, 0.0),
(-0.25, 1.0),
( 0.25,-1.0),
( 1.0, 0.0),
( 2.0, 0.0),
( 3.0, 0.0),
]
SCALE = 200
# Fully transparent brush used only to pad the composition's content bounds
# vertically, so the crop below stays consistent across tensions.
INVISIBLE_RENDER_STYLE = brushcue.RenderStyle.brush_only(
brushcue.Brush.solid(
brushcue.ProfiledColor.from_rgba_srgb(
brushcue.RGBAColor.from_components(0, 0, 0, 0)
),
0.001
)
)
In [2]:
# Overlay every tension from TENSION_POINTS on a single graph, so the effect
# of tension can be compared directly on the same set of control points.
OVERLAY_TENSION_COLORS = [
(-1.0, (0.0824, 0.3961, 0.7529)), # vivid blue
(-0.5, (0.0, 0.5373, 0.4824)), # vivid teal
( 0.0, (0.1804, 0.4902, 0.1961)), # vivid green
( 0.5, (0.9765, 0.6588, 0.1451)), # vivid amber
( 1.0, (0.7765, 0.1569, 0.1569)), # vivid red
]
POINT_COLOR = (0.15, 0.15, 0.15)
OVERLAY_SCALE = SCALE * 2
# Small margin (local units) beyond the outermost points, just enough to
# clear the point radius, so the crop cuts off right before the first/last dot.
X_MARGIN = 0.15
def run_overlay(key):
scale_transform = brushcue.Transform2.to_list(
brushcue.Transform2.identity().scale(brushcue.Vector2f.from_components(
OVERLAY_SCALE, OVERLAY_SCALE
))
)
painter = brushcue.Painter.new()
for tension, color in OVERLAY_TENSION_COLORS:
path = brushcue.Path.new()
path = path.move_to_point(brushcue.Point2f.from_components(
POINTS[0][0],
POINTS[0][1]
))
for point in POINTS[1:]:
path = path.cardinal_cubic_to_point(
brushcue.Point2f.from_components(
point[0],
point[1]
),
tension
)
line_render_style = brushcue.RenderStyle.brush_only(
brushcue.Brush.solid(
brushcue.ProfiledColor.from_rgba_srgb(
brushcue.RGBAColor.from_components(*color, 1)
),
0.006
)
)
painter = painter.add_path_with_render_style(path, line_render_style, scale_transform)
point_render_style = brushcue.RenderStyle.fill_only(
brushcue.Fill.solid(
brushcue.ProfiledColor.from_rgba_srgb(
brushcue.RGBAColor.from_components(*POINT_COLOR, 1)
),
)
)
for point in POINTS:
painter = painter.add_ellipse_with_render_style(
brushcue.Point2f.from_components(point[0], point[1]),
brushcue.Vector2f.from_components(0.1, 0.1),
0,
point_render_style,
scale_transform
)
bounds_path = brushcue.Path.new()
bounds_path = bounds_path.move_to_point(brushcue.Point2f.from_components(0.0, -1.5))
bounds_path = bounds_path.line_to_point(brushcue.Point2f.from_components(0.0, 1.5))
painter = painter.add_path_with_render_style(bounds_path, INVISIBLE_RENDER_STYLE, scale_transform)
composition = brushcue.Composition.painter(painter)
xs = [point[0] for point in POINTS]
x_min = min(xs) - X_MARGIN
x_max = max(xs) + X_MARGIN
composition = composition.crop(
brushcue.Bounds2f.from_x_y_width_height(
x_min * OVERLAY_SCALE,
-OVERLAY_SCALE * 1.1,
(x_max - x_min) * OVERLAY_SCALE,
OVERLAY_SCALE * 2 * 1.1 * 1.1,
)
)
result = composition.execute(ctx)
output_bytes = result.to_image_bytes(ctx)
output_path = f"{HOME}/{key}.png"
with open(output_path, "wb") as f:
f.write(output_bytes)
run_overlay("cardinal_cubic_overlay")
In [3]:
# A short line swatch per tension color, for use as a legend/key alongside
# the overlay chart.
KEY_NAMES = {
-1.0: "cardinal_cubic_neg_1_key",
-0.5: "cardinal_cubic_neg_05_key",
0.0: "cardinal_cubic_0_key",
0.5: "cardinal_cubic_05_key",
1.0: "cardinal_cubic_1_key",
}
def run_key(key, color):
path = brushcue.Path.new()
path = path.move_to_point(brushcue.Point2f.from_components(-0.5, 0.0))
path = path.line_to_point(brushcue.Point2f.from_components(0.5, 0.0))
line_render_style = brushcue.RenderStyle.brush_only(
brushcue.Brush.solid(
brushcue.ProfiledColor.from_rgba_srgb(
brushcue.RGBAColor.from_components(*color, 1)
),
0.02
)
)
scale_transform = brushcue.Transform2.to_list(
brushcue.Transform2.identity().scale(brushcue.Vector2f.from_components(
SCALE, SCALE
))
)
painter = brushcue.Painter.new()
painter = painter.add_path_with_render_style(path, line_render_style, scale_transform)
composition = brushcue.Composition.painter(painter)
composition = composition.crop(
brushcue.Bounds2f.from_x_y_width_height(
-SCALE * 0.6,
-SCALE * 0.1,
SCALE * 1.2,
SCALE * 0.2,
)
)
result = composition.execute(ctx)
output_bytes = result.to_image_bytes(ctx)
output_path = f"{HOME}/{key}.png"
with open(output_path, "wb") as f:
f.write(output_bytes)
for tension, color in OVERLAY_TENSION_COLORS:
run_key(KEY_NAMES[tension], color)