Skip to content

Instantly share code, notes, and snippets.

@gre
Last active January 19, 2023 08:08
Show Gist options
  • Save gre/fbe5007ac08513db84b95bc8c3b6579e to your computer and use it in GitHub Desktop.
Save gre/fbe5007ac08513db84b95bc8c3b6579e to your computer and use it in GitHub Desktop.
Current @greweb's set up to make a Plottable SVG from data (in TS, JS, Rust)

Usage

makeSVG({
  width: 100,
  height: 100,
  layers: [
    {
      color: "#e22",
      name: "Diamine Brilliant Red",
      strokeWidth: 0.5,
      routes: Array(40)
        .fill(0)
        .map((_, i) => [
          [10 + i, 10],
          [80 + i / 4, 90],
        ]),
    },
    {
      color: "deepskyblue", // also a valid CSS color
      name: "Diamine Turquoise",
      strokeWidth: 0.5,
      routes: Array(40)
        .fill(0)
        .map((_, i) => [
          [90 - i, 10],
          [20 - i / 4, 90],
        ]),
    },
  ],
});

Result:

svg

notice the usage of "multiply" reproduces ink blending pretty accurately. (real world example: https://greweb.me/plots/291)

License of the code shared here:

WTFPL – Do What the Fuck You Want to Public License

// a series of x,y coordinates to draw lines through them
type PathData = Array<[number, number]>;
// all the data to make a layer
type SVGDataLayer = {
// this is the CSS color that try to match the ink color you will use
color: string,
// this is the name of the layer. usually the ink color name.
name: string,
// this can try to match the pen stroke width so the SVG looks like the output
strokeWidth: number,
// this stores all the paths to draw. each separate PathData are not connected
routes: PathData[],
};
type SVGData = {
layers: SVGDataLayer[],
width: number, // width in millimeters
height: number, // height in millimeters
background?: string,
};
function makeLayer(l: SVGDataLayer, index: number) {
const body = l.routes
.map(
(route) =>
`<path d="${route
.map(
([x, y], i) =>
`${i === 0 ? "M" : "L"}${x.toFixed(2)},${y.toFixed(2)}` // toFixed will makes the svg really smaller (we don't need all the digits)
)
.join(" ")}" fill="none" stroke="${l.color}" stroke-width="${
l.strokeWidth
}" style="mix-blend-mode: multiply;" />` // usage of "multiply" allows to reproduce the effect of ink blending into each other
)
.join("\n");
return `<g inkscape:groupmode="layer" inkscape:label="${index} ${l.name}">${body}</g>`;
}
// Bake the SVG from scratch
function makeSVG(a: SVGData) {
let body = a.layers.map(makeLayer).join("\n");
return `<svg style="background:${a.background||"white"}" viewBox="0 0 ${a.width} ${a.height}" width="${a.width}mm" height="${a.height}mm" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
${body}
</svg>`;
}
// JS compiled version (if you prefer)
function makeLayer(l) {
const body = l.routes
.map((route) => `<path d="${route
.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(2)},${y.toFixed(2)}`)
.join(" ")}" fill="none" stroke="${l.color}" stroke-width="${l.strokeWidth}" style="mix-blend-mode: multiply;" />`)
.join("\n");
return `<g inkscape:groupmode="layer" inkscape:label="${l.name}">${body}</g>`;
}
function makeSVG(a) {
let body = a.layers.map(makeLayer).join("\n");
return `<svg style="background:${a.background||"white"}" viewBox="0 0 ${a.width} ${a.height}" width="${a.width}mm" height="${a.height}mm" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
${body}
</svg>`;
}
type PathData = Vec<(f64, f64)>;
struct SVGDataLayer {
color: String,
name: String,
stroke_width: f64,
routes: Vec<PathData>,
}
struct SVGData {
layers: Vec<SVGDataLayer>,
width: f64,
height: f64,
background: Option<String>,
}
fn make_layer(l: &SVGDataLayer, index: usize) -> String {
let body = l.routes
.iter()
.map(|route| {
let path_data = route
.iter()
.enumerate()
.map(|(i, (x, y))| {
format!("{} {}, {}", if i == 0 { "M" } else { "L" }, x.to_string(), y.to_string())
})
.collect::<Vec<String>>()
.join(" ");
format!("<path d=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{}\" style=\"mix-blend-mode: multiply;\" />", path_data, l.color, l.stroke_width)
})
.collect::<Vec<String>>()
.join("\n");
format!("<g inkscape:groupmode=\"layer\" inkscape:label=\"{} {}\">{}</g>", index, l.name, body)
}
fn make_svg(a: &SVGData) -> String {
let body = a.layers
.iter()
.enumerate()
.map(|(i, l)| make_layer(l, i))
.collect::<Vec<String>>()
.join("\n");
let background = match &a.background {
Some(color) => color,
None => "white"
};
format!("<svg style=\"background:{}\" viewBox=\"0 0 {} {}\" width=\"{}mm\" height=\"{}mm\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\">\n{}\n</svg>", background, a.width, a.height, a.width, a.height, body)
}
/*
fn main() {
let data = SVGData {
width: 100.0,
height: 100.0,
layers: vec![
SVGDataLayer {
color: "#e22".to_string(),
name: "Diamine Brilliant Red".to_string(),
stroke_width: 0.5,
routes: (0..40)
.map(|i| vec![(10.0 + i as f64, 10.0), (80.0 + i as f64 / 4.0, 90.0)])
.collect::<Vec<PathData>>(),
},
SVGDataLayer {
color: "deepskyblue".to_string(),
name: "Diamine Turquoise".to_string(),
stroke_width: 0.5,
routes: (0..40)
.map(|i| vec![(90.0 - i as f64, 10.0), (20.0 - i as f64 / 4.0, 90.0)])
.collect::<Vec<PathData>>(),
},
],
background: None,
};
println!("{}", make_svg(&data));
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment