Skip to content

Commit

Permalink
✨ Hatched patterns (bottom-up only)
Browse files Browse the repository at this point in the history
  • Loading branch information
gwennlbh committed May 3, 2024
1 parent 4436161 commit e1b7c54
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 90 deletions.
3 changes: 3 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ install:

example-video args='':
./shapemaker video --colors colorschemes/palenight.css out.mp4 --sync-with fixtures/schedule-hell.midi --audio fixtures/schedule-hell.flac --grid-size 16x10 --resolution 1920 {{args}}

example-image args='':
./shapemaker image --colors colorschemes/palenight.css out.svg {{args}}
7 changes: 7 additions & 0 deletions src/anchors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ impl Anchor {
self.0 += dx;
self.1 += dy;
}

pub fn distances(&self, other: &Anchor) -> (usize, usize) {
(
self.0.abs_diff(other.0) as usize,
self.1.abs_diff(other.1) as usize,
)
}
}

impl From<(i32, i32)> for Anchor {
Expand Down
54 changes: 39 additions & 15 deletions src/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::{cmp, collections::HashMap, io::Write, ops::Range};
use chrono::DateTime;
use itertools::Itertools;
use rand::Rng;
use svg::node::element::Pattern;

use crate::{
layer::Layer, objects::Object, random_color, web::console_log, Anchor, CenterAnchor, Color,
Expand Down Expand Up @@ -364,7 +365,7 @@ impl Canvas {
self.remove_background()
}

pub fn save_as_png(
pub fn save_as(
at: &str,
aspect_ratio: f32,
resolution: usize,
Expand All @@ -377,6 +378,7 @@ impl Canvas {
// portrait: resolution is height
((resolution as f32 / aspect_ratio) as usize, resolution)
};

let mut spawned = std::process::Command::new("magick")
.args(["-background", "none"])
.args(["-size", &format!("{}x{}", width, height)])
Expand Down Expand Up @@ -417,6 +419,20 @@ impl Canvas {
.collect()
}

fn unique_pattern_fills(&self) -> Vec<Fill> {
self.layers
.iter()
.flat_map(|layer| {
layer
.objects
.iter()
.flat_map(|(_, o)| o.1.map(|fill| fill.clone()))
})
.filter(|fill| matches!(fill, Fill::Hatched(..)))
.unique_by(|fill| fill.pattern_id())
.collect()
}

pub fn render(&mut self, layers: &Vec<&str>, render_background: bool) -> String {
let background_color = self.background.unwrap_or_default();
let mut svg = svg::Document::new();
Expand All @@ -427,7 +443,7 @@ impl Canvas {
.set("y", -(self.canvas_outter_padding as i32))
.set("width", self.width())
.set("height", self.height())
.set("fill", background_color.to_string(&self.colormap)),
.set("fill", background_color.render(&self.colormap)),
);
}
for layer in self
Expand All @@ -439,22 +455,30 @@ impl Canvas {
svg = svg.add(layer.render(self.colormap.clone(), self.cell_size, layer.object_sizes));
}

let mut defs = svg::node::element::Definitions::new();
for filter in self.unique_filters() {
svg = svg.add(filter.definition())
defs = defs.add(filter.definition())
}

svg.set(
"viewBox",
format!(
"{0} {0} {1} {2}",
-(self.canvas_outter_padding as i32),
self.width(),
self.height()
),
)
.set("width", self.width())
.set("height", self.height())
.to_string()
for pattern_fill in self.unique_pattern_fills() {
if let Some(patterndef) = pattern_fill.pattern_definition(&self.colormap) {
defs = defs.add(patterndef)
}
}

svg.add(defs)
.set(
"viewBox",
format!(
"{0} {0} {1} {2}",
-(self.canvas_outter_padding as i32),
self.width(),
self.height()
),
)
.set("width", self.width())
.set("height", self.height())
.to_string()
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use std::{
path::PathBuf,
};

use serde::Deserialize;
use rand::Rng;
use serde::Deserialize;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
Expand Down Expand Up @@ -72,7 +72,7 @@ impl From<&str> for Color {
}

impl Color {
pub fn to_string(self, mapping: &ColorMapping) -> String {
pub fn render(self, mapping: &ColorMapping) -> String {
match self {
Color::Black => mapping.black.to_string(),
Color::White => mapping.white.to_string(),
Expand Down Expand Up @@ -259,7 +259,7 @@ impl ColorMapping {

fn from_css_line(&mut self, line: &str) {
if let Some((name, value)) = line.trim().split_once(":") {
let value = value.trim().to_owned();
let value = value.trim().trim_end_matches(";").to_owned();
match name.trim() {
"black" => self.black = value,
"white" => self.white = value,
Expand Down
158 changes: 111 additions & 47 deletions src/fill.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
use std::hash::Hash;

use crate::{Color, ColorMapping, RenderCSS};

#[derive(Debug, Clone, Copy)]
pub enum Fill {
Solid(Color),
Translucent(Color, f32),
Hatched(Color, HatchDirection, f32, f32),
Dotted(Color, f32),
}

#[derive(Debug, Clone, Copy)]
pub enum HatchDirection {
Horizontal,
Expand All @@ -8,73 +18,35 @@ pub enum HatchDirection {
TopDownDiagonal,
}

impl HatchDirection {
pub fn svg_filter_name(&self) -> String {
"hatch-".to_owned()
+ match self {
HatchDirection::Horizontal => "horizontal",
HatchDirection::Vertical => "vertical",
HatchDirection::BottomUpDiagonal => "bottom-up",
HatchDirection::TopDownDiagonal => "top-down",
}
}

pub fn svg_pattern_definition(&self) -> String {
// https://stackoverflow.com/a/14500054/9943464
format!(
r#"<pattern id="{}" patternUnits="userSpaceOnUse" width="{}" height="{}">"#,
self.svg_filter_name(),
todo!(),
todo!()
) + &match self {
HatchDirection::BottomUpDiagonal => format!(
r#"<path
d="M-1,1 l2,-2
M0,4 l4,-4
M3,5 l2,-2"
style="stroke:black; stroke-width:1"
/>"#
),
HatchDirection::Horizontal => todo!(),
HatchDirection::Vertical => todo!(),
HatchDirection::TopDownDiagonal => todo!(),
} + "</pattern>"
}
}
const PATTERN_SIZE: usize = 8;

#[derive(Debug, Clone, Copy)]
pub enum Fill {
Solid(Color),
Translucent(Color, f32),
Hatched(HatchDirection),
Dotted(f32),
}
impl HatchDirection {}

impl RenderCSS for Fill {
fn render_fill_css(&self, colormap: &ColorMapping) -> String {
match self {
Fill::Solid(color) => {
format!("fill: {};", color.to_string(colormap))
format!("fill: {};", color.render(colormap))
}
Fill::Translucent(color, opacity) => {
format!("fill: {}; opacity: {};", color.to_string(colormap), opacity)
format!("fill: {}; opacity: {};", color.render(colormap), opacity)
}
Fill::Dotted(radius) => unimplemented!(),
Fill::Hatched(direction) => {
format!("fill: url(#{});", direction.svg_filter_name())
Fill::Dotted(..) => unimplemented!(),
Fill::Hatched(..) => {
format!("fill: url(#{});", self.pattern_id())
}
}
}

fn render_stroke_css(&self, colormap: &ColorMapping) -> String {
match self {
Fill::Solid(color) => {
format!("stroke: {}; fill: transparent;", color.to_string(colormap))
format!("stroke: {}; fill: transparent;", color.render(colormap))
}
Fill::Translucent(color, opacity) => {
format!(
"stroke: {}; opacity: {}; fill: transparent;",
color.to_string(colormap),
color.render(colormap),
opacity
)
}
Expand All @@ -84,6 +56,98 @@ impl RenderCSS for Fill {
}
}

impl Fill {
pub fn pattern_name(&self) -> String {
match self {
Fill::Hatched(_, direction, ..) => format!(
"hatched-{}",
match direction {
HatchDirection::Horizontal => "horizontal",
HatchDirection::Vertical => "vertical",
HatchDirection::BottomUpDiagonal => "bottom-up",
HatchDirection::TopDownDiagonal => "top-down",
}
),
_ => String::from(""),
}
}

pub fn pattern_id(&self) -> String {
if let Fill::Hatched(color, _, thickness, spacing) = self {
return format!(
"pattern-{}-{}-{}",
self.pattern_name(),
color.name(),
thickness
);
}
String::from("")
}

pub fn pattern_definition(
&self,
colormapping: &ColorMapping,
) -> Option<svg::node::element::Pattern> {
match self {
Fill::Hatched(color, direction, size, thickness_ratio) => {
let root = svg::node::element::Pattern::new()
.set("id", self.pattern_id())
.set("patternUnits", "userSpaceOnUse");

let thickness = size * (2.0 * thickness_ratio);
// TODO: to re-center when tickness ratio != ½
let offset = 0.0;

Some(match direction {
HatchDirection::BottomUpDiagonal => root
// https://stackoverflow.com/a/74205714/9943464
/*
<polygon points="0,0 4,0 0,4" fill="yellow"></polygon>
<polygon points="0,8 8,0 8,4 4,8" fill="yellow"></polygon>
<polygon points="0,4 0,8 8,0 4,0" fill="green"></polygon>
<polygon points="4,8 8,8 8,4" fill="green"></polygon>
*/
.add(
svg::node::element::Polygon::new()
.set(
"points",
format!(
"0,0 {},0 0,{}",
offset + thickness / 2.0,
offset + thickness / 2.0
),
)
.set("fill", color.render(colormapping)),
)
.add(
svg::node::element::Polygon::new()
.set(
"points",
format!(
"0,{} {},0 {},{} {},{}",
offset + size,
offset + size,
offset + size,
offset + thickness / 2.0,
offset + thickness / 2.0,
offset + size,
),
)
.set("fill", color.render(colormapping)),
)
.set("height", size * 2.0)
.set("width", size * 2.0)
.set("viewBox", format!("0,0,{},{}", size, size)),
HatchDirection::Horizontal => todo!(),
HatchDirection::Vertical => todo!(),
HatchDirection::TopDownDiagonal => todo!(),
})
}
_ => None,
}
}
}

impl RenderCSS for Option<Fill> {
fn render_fill_css(&self, colormap: &ColorMapping) -> String {
self.map(|fill| fill.render_fill_css(colormap))
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ impl<AdditionalContext: Default> Video<AdditionalContext> {
aspect_ratio: f32,
resolution: usize,
) -> Result<(), String> {
Canvas::save_as_png(
Canvas::save_as(
&format!(
"{}/{:0width$}.png",
frames_output_directory,
Expand Down
Loading

0 comments on commit e1b7c54

Please sign in to comment.