diff --git a/CHANGELOG.md b/CHANGELOG.md index bdeeded..e145d40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# v2.2.2 + +- correct levo/dextro for variable-body length capsids +- add pinwheel tiles + # v2.2.1 - only re-render for non-svg export diff --git a/README.md b/README.md index 14f0d7f..391ff79 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This work implements Caspar-Klug Theory to generate high-quality, vectorized cap # Run -- Run democapsid (v2.2.1): [https://dnanto.github.io/democapsid/app.html](https://dnanto.github.io/democapsid/app.html). +- Run democapsid (v2.2.2): [https://dnanto.github.io/democapsid/app.html](https://dnanto.github.io/democapsid/app.html). ![screenshot.png](screenshot.png) diff --git a/TODO.md b/TODO.md index 64214be..7418723 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,6 @@ -- [ ] update paper -- [ ] tests -- [ ] comments/documentation javascript +- [~] update paper +- [~] comments/documentation javascript +- [?] tests - [?] R package - [x] comments/documentation python - [x] python package diff --git a/app.html b/app.html index 3836259..ff95a49 100644 --- a/app.html +++ b/app.html @@ -12,7 +12,7 @@
diff --git a/js/app.js b/js/app.js index a1581d9..c5f993e 100644 --- a/js/app.js +++ b/js/app.js @@ -47,7 +47,7 @@ const DEFAULTS = Object.assign( function params() { const PARAMS = Object.fromEntries(Object.keys(DEFAULTS).map((k) => [k, DEFAULTS[k](document.getElementById(k))])); - PARAMS.c = PARAMS.l ? "levo" : "dextro"; + PARAMS.c = PARAMS.l; return PARAMS; } @@ -61,7 +61,7 @@ function params_to_tag(PARAMS) { "R=" + PARAMS.R, "t=" + PARAMS.t, `s=${(PARAMS.s * 100).toFixed(2)}%`, - "c=" + PARAMS.c, + "c=" + (PARAMS.c ? "levo" : "dextro"), "@(" + [PARAMS.θ, PARAMS.ψ, PARAMS.φ].map((e) => e + "°").join(",") + ")", ].join(","); } @@ -160,7 +160,6 @@ function update(e) { `T-Number=(${h})²+(${h})(${k})+(${k})²=${h * h + h * k + k * k}`, `Q-Number=(${H})²+(${H})(${K})+(${K})²=${H * H + H * K + K * K}`, ].join("\n"); - if (PARAMS.c === "levo") paper.view.scale(1, -1); } catch (e) { console.log(e); paper.clear(); diff --git a/js/democapsid.js b/js/democapsid.js index b5770e6..1ebcc44 100644 --- a/js/democapsid.js +++ b/js/democapsid.js @@ -1,10 +1,10 @@ /*! - * democapsid v2.2.1 - Render viral capsids in the browser and export SVG. + * democapsid v2.2.2 - Render viral capsids in the browser and export SVG. * MIT License * Copyright (c) 2020 - 2024, Daniel Antonio Negrón (dnanto/remaindeer) */ -const VERSION = "2.2.1"; +const VERSION = "2.2.2"; const SQRT3 = Math.sqrt(3); const SQRT5 = Math.sqrt(5); @@ -446,6 +446,48 @@ function calc_tile(t, R) { ), radius: R, }; + } else if (t === "pinwheel-1") { + tile = { + basis: [ + [1.5 * r, 0.75 * R], + [0, 1.5 * R], + ], + tile: (e) => + Array.from({ length: 6 }, (_, i) => + new paper.Path({ + segments: [ + [0, 0], + [0, R], + [-r / 2, 0.75 * R], + [-r / 2, 0.25 * R], + ].map((f) => e.coor.add(f)), + closed: true, + data: { mer: 1 }, + }).rotate(i * 60, e.coor) + ), + radius: r, + }; + } else if (t === "pinwheel-2") { + tile = { + basis: [ + [1.5 * r, 0.75 * R], + [0, 1.5 * R], + ], + tile: (e) => + Array.from({ length: 6 }, (_, i) => + new paper.Path({ + segments: [ + [0, 0], + [0, R], + [r / 2, 0.75 * R], + [r / 2, 0.25 * R], + ].map((f) => e.coor.add(f)), + closed: true, + data: { mer: 1 }, + }).rotate(i * 60, e.coor) + ), + radius: r, + }; } else { throw new Error("incorrect tile mode"); } @@ -466,16 +508,29 @@ function* tile_grid(ck, basis) { } } -function ck_vectors(basis, h, k, H, K) { +function ck_vectors(basis, h, k, H, K, c) { const [v1, v2] = basis; const v3 = v2.rot(Math.PI / 3); - return [ - // - v1.mul(h).add(v2.mul(k)), - v2.mul(H).add(v3.mul(K)), - v1.mul(-h - k).add(v2.mul(h)), - v1.mul(k).add(v3.mul(-h)), - ]; + if (c) { + return [ + // levo + v1.mul(h).add(v2.mul(k)), + v2.mul(H).add(v3.mul(K)), + v3.mul(h).add(v1.mul(-k)), + v1.mul(k).add(v3.mul(-h)), + ]; + } else { + return [ + // dextro + v1.mul(h).add(v3.mul(-k)), + v2.mul(H).add(v1.mul(K)), + v3.mul(h).add(v2.mul(k)), + v1 + .mul(h) + .add(v3.mul(-k)) + .rot(-Math.PI / 3), + ]; + } } function triangle_circumcircle_center(p, q, r) { @@ -719,7 +774,6 @@ function ico_axis_3(ck, iter = ITER, tol = TOL) { const [p, q] = [pB.add(o.proj(v)), pB.add(o)]; [v, k] = [q.sub(p), pB.sub(pD).uvec()]; const f = (t) => c - p.add(v.roro(k, t)).sub(pF).norm(); - let br = brackets(f, 0, 2 * Math.PI, iter).next().value; t = bisection(f, ...brackets(f, 0, 2 * Math.PI, iter).next().value, tol, iter).slice(-1); const pG = p.add(v.roro(k, t)); return [pD, pF, pG, Math.abs(pD[1]) - pG.sub([0, 0, pG[2]]).norm()]; @@ -830,34 +884,38 @@ function ico_axis_2(ck, iter = ITER, tol = TOL) { } function model_sa_error(PARAMS) { - const tile = calc_tile(PARAMS.t, PARAMS.R); - const ck = ck_vectors(tile.basis, PARAMS.h, PARAMS.k, PARAMS.H, PARAMS.K); - const triangles = [ - [ck[3], ck[0]], - [ck[0], ck[1]], - [ck[1], ck[2]], - ]; - // coordinates - const ico_coors = ["", "", ico_axis_2, ico_axis_3, "", ico_axis_5][PARAMS.a](ck, ITER, TOL); - const config = ico_config(PARAMS.a); - - const n_tris = config.t_id.map((e) => parseInt(e[1])).reduce((a, b) => (a < b ? b : a)); - const n_per_tri = Array.from({ length: n_tris }).fill(0); - config.t_id.forEach((e, i) => (n_per_tri[e[1] - 1] += config.t_rep[i])); - const sa_net = triangles - .slice(0, n_tris) - .map((e, i) => ([...e[0], 0].cross([...e[1], 0]).norm() / 2) * n_per_tri[i]) - .sum(); - const sa_capsid = config.v_idx - .map((e) => e.map((i) => ico_coors[i])) - .map((e, i) => (e[1].sub(e[0]).cross(e[2].sub(e[0])).norm() / 2) * config.t_rep[i]) - .sum(); - return (sa_net - sa_capsid) / sa_net; + try { + const tile = calc_tile(PARAMS.t, PARAMS.R); + const ck = ck_vectors(tile.basis, PARAMS.h, PARAMS.k, PARAMS.H, PARAMS.K, PARAMS.c); + const triangles = [ + [ck[3], ck[0]], + [ck[0], ck[1]], + [ck[1], ck[2]], + ]; + // coordinates + const ico_coors = ["", "", ico_axis_2, ico_axis_3, "", ico_axis_5][PARAMS.a](ck, ITER, TOL); + const config = ico_config(PARAMS.a); + + const n_tris = config.t_id.map((e) => parseInt(e[1])).reduce((a, b) => (a < b ? b : a)); + const n_per_tri = Array.from({ length: n_tris }).fill(0); + config.t_id.forEach((e, i) => (n_per_tri[e[1] - 1] += config.t_rep[i])); + const sa_net = triangles + .slice(0, n_tris) + .map((e, i) => ([...e[0], 0].cross([...e[1], 0]).norm() / 2) * n_per_tri[i]) + .sum(); + const sa_capsid = config.v_idx + .map((e) => e.map((i) => ico_coors[i])) + .map((e, i) => (e[1].sub(e[0]).cross(e[2].sub(e[0])).norm() / 2) * config.t_rep[i]) + .sum(); + return (sa_net - sa_capsid) / sa_net; + } catch (_) { + return NaN; + } } -function lattice_config(h, k, H, K, R, t) { +function lattice_config(h, k, H, K, c, R, t) { const tile = calc_tile(t, R); - const ck = ck_vectors(tile.basis, h, k, H, K); + const ck = ck_vectors(tile.basis, h, k, H, K, c); // grid //// calculate @@ -869,7 +927,7 @@ function lattice_config(h, k, H, K, R, t) { .concat([[0, 0]]); //// metadata lattice.flat().forEach((e) => { - e.data.offset = e.data.mer + (vertex_coordinates.some((v) => [e.position.x, e.position.y].sub(v).norm() <= tile.radius) ? 0 : 3); + e.data.offset = e.data.mer + (vertex_coordinates.some((v) => [e.position.x, e.position.y].sub(v).norm() - tile.radius < TOL) ? 0 : 3); e.data.centroid = e.segments .map((e) => e.point) .reduce((a, b) => a.add(b)) @@ -926,9 +984,9 @@ function calc_facets(lat_cfg, PARAMS) { function draw_lattice(PARAMS) { // unpack - const [h, k, H, K, R, t] = ["h", "k", "H", "K", "R", "t"].map((e) => PARAMS[e]); + const [h, k, H, K, c, R, t] = ["h", "k", "H", "K", "c", "R", "t"].map((e) => PARAMS[e]); // lattice - const lat_cfg = lattice_config(h, k, H, K, R, t); + const lat_cfg = lattice_config(h, k, H, K, c, R, t); lat_cfg.lattice.flat().forEach((e) => (e.style.fillColor = PARAMS["mer_color_" + e.data.offset] + PARAMS["mer_alpha_" + e.data.offset])); return new paper.Group( new paper.Group({ @@ -941,16 +999,16 @@ function draw_lattice(PARAMS) { strokeJoin: "round", }, }) - ); + ).scale(1, -1); } function draw_facets(PARAMS) { // unpack - const [h, k, H, K, R, t] = ["h", "k", "H", "K", "R", "t"].map((e) => PARAMS[e]); + const [h, k, H, K, c, R, t] = ["h", "k", "H", "K", "c", "R", "t"].map((e) => PARAMS[e]); // lattice - const lat_cfg = lattice_config(h, k, H, K, R, t); - const g = new paper.Group({ children: calc_facets(lat_cfg, PARAMS), position: paper.view.center }); + const lat_cfg = lattice_config(h, k, H, K, c, R, t); + const g = new paper.Group({ children: calc_facets(lat_cfg, PARAMS), position: paper.view.center }).scale(1, -1); lat_cfg.lattice.forEach((e) => e.forEach((f) => f.remove())); @@ -984,87 +1042,46 @@ function draw_facets(PARAMS) { function draw_net(PARAMS) { // unpack - const [h, k, H, K, R, t] = ["h", "k", "H", "K", "R", "t"].map((e) => PARAMS[e]); + const [h, k, H, K, c, R, t] = ["h", "k", "H", "K", "c", "R", "t"].map((e) => PARAMS[e]); // lattice - const lat_cfg = lattice_config(h, k, H, K, R, t); + const lat_cfg = lattice_config(h, k, H, K, c, R, t); const ck = lat_cfg.ck; const facets = calc_facets(lat_cfg, PARAMS); let g; - /****/ if (PARAMS.a === 5) { - const unit1 = new paper.Group(facets.slice(0, 2)).rotate(-degrees(ck[0].angle([1, 0]))).scale(-1, 1); - const unit2 = unit1.clone().rotate(180); - const top_center = unit2.children[1].children - .flatMap((e) => e.segments) - .map((e) => e.point) - .reduce((a, b) => (a.y < b.y ? a : b)); - unit2.position.x += unit1.children[0].bounds.right - top_center.x; - unit2.position.y += unit1.children[0].bounds.height; + if (PARAMS.a == 5) { + const u = new paper.Group(facets.slice(0, 2).map((e) => e.clone())); + const v = u + .clone() + .rotate(180, ck[0]) + .translate(ck[1].sub(ck[0].mul(2))); g = new paper.Group({ - children: Array.from({ length: 5 }) - .flatMap((_, i) => { - const [u1, u2] = [unit1.clone(), unit2.clone()]; - u1.position.x += i * unit1.children[0].bounds.width; - u2.position.x += i * unit1.children[0].bounds.width; - return [u1, u2]; - }) - .flatMap((e) => e.children), + children: [u, v].flatMap((e) => Array.from({ length: 5 }, (_, i) => e.clone().translate(ck[0].mul(i)))).flatMap((e) => e.children), position: paper.view.center, - }); - // clean-up - [unit1, unit2].forEach((e) => e.remove()); - } else if (PARAMS.a === 3) { - const angle = ck[0].angle([1, 0]); - const unit1 = new paper.Group(facets).rotate(-degrees(angle)).scale(-1, 1); - const unit0 = facets[0].clone().rotate(180); - unit0.position.x += ck[0].norm() / 2; - const centroid = [unit0.bounds.topLeft, unit0.bounds.topRight, unit0.bounds.bottomCenter].reduce((a, b) => a.add(b)).divide(3); - const center1 = unit0.bounds.topRight.add(new paper.Point([1, 0].mul(ck[1].norm()).rot(-(Math.PI / 3 - ck[1].angle(ck[2]))))); - const center2 = unit0.bounds.topRight.add(new paper.Point([1, 0].mul(ck[2].norm()).rot(-(Math.PI / 3 - ck[1].angle(ck[2]) + ck[1].angle(ck[2]))))); - const f = new paper.Group([...[1, 2, 3].map((_, i) => unit1.clone().rotate(i * 120, centroid))]); - f.children.slice(0, -1).forEach((e) => e.children[1].remove()); - const unit2 = f.clone().rotate(180); - unit2.bounds.left = Math.min(center1.x, center2.x); - unit2.bounds.bottom = unit0.bounds.topRight.y; - unit2.position.y -= unit2.children[1].children[0].bounds.bottom - center1.y; - unit2.children.forEach((e) => f.addChild(e.clone())); - f.position = paper.view.center; + }).rotate(-degrees(Math.atan2(ck[0].dot([0, 1]), ck[0][0] * 1 - ck[0][1] * 0))); + [u, v].forEach((e) => e.remove()); + } else if (PARAMS.a == 3) { + const centroid = [[0, 0], ck[0], ck[3]].centroid(); + const u = new paper.Group(facets.slice(0, 1).map((e) => e.clone())); + const v = new paper.Group(facets.slice(0, 3).map((e) => e.clone())).translate(ck[0]).rotate(-60, ck[0]); + const w = new paper.Group([u.clone(), ...Array.from({ length: 3 }, (_, i) => v.clone().rotate(i * 120, centroid))]); + const p = ck[0].add(ck[1].rot(Math.PI / 3).rot((4 * Math.PI) / 3)); g = new paper.Group({ - children: f.children.flatMap((e) => e.children.flat()), - }); - // clean-up - [unit0, unit1, unit2].forEach((e) => e.remove()); - } else if (PARAMS.a === 2) { - const angle = ck[0].angle([1, 0]); - const unit1 = new paper.Group(facets).rotate(-degrees(angle)).rotate(-60).scale(-1, 1); - const unit2 = unit1.children[1].clone(); - const vector = unit1.children[0].bounds.topLeft.subtract(unit1.children[0].bounds.bottomCenter); - unit2.position = unit2.position.add(vector); - const unit3 = new paper.Group([unit1.clone(), unit1.children[0].clone().rotate(60, unit1.children[0].bounds.topRight), unit2.clone()]); - const unit4 = unit3.clone().rotate(180, unit3.children[0].children[0].bounds.topRight); - unit4.position = unit4.position.add(vector.rotate(240)); - const unit5 = new paper.Group([unit3, unit4]); - const point1 = unit5.children[0].children[2].children - .filter((e) => e.data.offset === 1) - .flatMap((e) => e.segments.map((e) => e.point)) - .filter((e) => Math.abs(e.getDistance(unit5.children[0].children[1].bounds.bottomLeft) - ck[1].norm()) < 1e-5) - .reduce((a, b) => (a.y < b.y ? b : a)); - const unit6 = unit5.clone(); - unit6.position = unit6.position.add(unit5.children[1].children[0].children[0].bounds.bottomRight.subtract(point1)); - const f = new paper.Group([unit5, unit6]); - f.position = paper.view.center; - const children = f.children.flatMap((e) => e.children).flatMap((e) => e.children); + children: [w.clone(), w.clone().translate(ck[0].add(p)).rotate(60, p)].flatMap((e) => e.children).flatMap((e) => e.children), + position: paper.view.center, + }).rotate(-degrees(Math.atan2(ck[0].dot([0, 1]), ck[0][0] * 1 - ck[0][1] * 0)) - 30); + [u, v, w].forEach((e) => e.remove()); + } else if (PARAMS.a == 2) { + const u = new paper.Group(facets.slice(0, 3).map((e) => e.clone())); + const v = new paper.Group([...u.clone().children, u.children[0].clone().rotate(60, ck[0]), u.children[2].clone().translate(ck[0])]); + const w = new paper.Group([v.clone(), v.clone().rotate(180, ck[3]).translate(ck[3].mul(-1))]); g = new paper.Group({ - children: [...children.filter((_, i) => i % 3 === 0).flatMap((e) => e.children), ...children.filter((_, i) => i % 3 !== 0)], - }); - // clean-up - [2, 5, 8, 11].forEach((e) => g.children[e].remove()); - [unit1, unit2, unit3, unit4, unit5, unit6].forEach((e) => e.remove()); - } else { - throw new Error("invalid symmetry mode!"); + children: [w.clone(), w.clone().translate(ck[1].mul(-1).add(ck[0].mul(-2)).add(ck[3]))].flatMap((e) => e.children).flatMap((e) => e.children), + position: paper.view.center, + }).rotate(-degrees(Math.atan2(ck[0].dot([0, 1]), ck[0][0] * 1 - ck[0][1] * 0)) - 30); + [u, v, w].forEach((e) => e.remove()); } - g.scale(-1, 1); // clean-up facets.forEach((e) => e.remove()); @@ -1075,11 +1092,11 @@ function draw_net(PARAMS) { g.style.strokeWidth = PARAMS.line_size; g.style.strokeCap = "round"; g.style.strokeJoin = "round"; - return g; + return g.scale(1, -1); } g.remove(); return new paper.Group({ - children: g.children.map( + children: g.children.flatMap( (e) => new paper.Group( e.children.map((e) => { @@ -1095,15 +1112,15 @@ function draw_net(PARAMS) { strokeCap: "round", strokeJoin: "round", }, - }); + }).scale(1, -1); } function draw_capsid(PARAMS) { // unpack - const [h, k, H, K, R, t] = ["h", "k", "H", "K", "R", "t"].map((e) => PARAMS[e]); + const [h, k, H, K, c, R, t] = ["h", "k", "H", "K", "c", "R", "t"].map((e) => PARAMS[e]); // lattice - const lat_cfg = lattice_config(h, k, H, K, R, t); + const lat_cfg = lattice_config(h, k, H, K, c, R, t); const facets = calc_facets(lat_cfg, PARAMS); // coordinates @@ -1233,7 +1250,7 @@ function draw_capsid(PARAMS) { }); g.style.strokeCap = "round"; g.style.strokeJoin = "round"; - return g; + return g.scale(-1, 1); } g.remove(); return new paper.Group({ @@ -1259,7 +1276,7 @@ function draw_capsid(PARAMS) { strokeCap: "round", strokeJoin: "round", }, - }); + }).scale(-1, 1); } if (typeof exports !== "undefined") { diff --git a/js/tiles.js b/js/tiles.js new file mode 100644 index 0000000..5d2abe8 --- /dev/null +++ b/js/tiles.js @@ -0,0 +1,130 @@ + } else if (t === "trunhex") { + const a12 = (SQRT2 * R) / (1 + SQRT3); + const r12 = a12 * ((2 + SQRT3) / 2); + const h3_12 = (SQRT3 / 2) * a12; + const r3_12 = (SQRT3 / 6) * a12; + tile = { + basis: [ + [2 * r12, 0], + [r12, r12 + 0.5 * a12 + h3_12], + ], + tile: (e) => [ + new paper.Path.RegularPolygon({ + center: e.coor, + sides: 12, + radius: R, + data: { mer: 1 }, + }).rotate(15), + ...Array.from({ length: 2 }, (_, i) => + new paper.Path.RegularPolygon({ + center: e.coor.add([0, -(r12 + r3_12)]), + sides: 3, + radius: a12 / SQRT3, + data: { mer: 2 }, + }).rotate(i * 60, e.coor) + ), + ], + radius: R + h3_12, + }; + } else if (t === "triakistri") { + tile = { + basis: [ + [1.5 * R, r], + [0, 2 * r], + ], + tile: (e) => [ + ...Array.from({ length: 6 }, (_, i) => + new paper.Path({ + segments: [ + [-R / 2, r], + [+R / 2, r], + [0, R / SQRT3], + ].map((f) => e.coor.add(f)), + closed: true, + data: { mer: 1 }, + }).rotate(i * 60, e.coor) + ), + ...Array.from({ length: 6 }, (_, i) => + new paper.Path({ + segments: [ + [-R / 2, r], + [0, R / SQRT3], + [0, 0], + ].map((f) => e.coor.add(f)), + closed: true, + data: { mer: 1 }, + }).rotate(i * 60, e.coor) + ), + ...Array.from({ length: 6 }, (_, i) => + new paper.Path({ + segments: [ + [+R / 2, r], + [0, R / SQRT3], + [0, 0], + ].map((f) => e.coor.add(f)), + closed: true, + data: { mer: 1 }, + }).rotate(i * 60, e.coor) + ), + ], + radius: R, + }; + } else if (t === "truntrihex") { + const a12 = (SQRT2 * R) / (1 + SQRT3); + const r12 = a12 * ((2 + SQRT3) / 2); + const h3_12 = (SQRT3 / 2) * a12; + tile = { + basis: [ + [2 * r12 + a12, 0], + [r12 + a12 / 2, a12 / 2 + 2 * h3_12 + r12], + ], + tile: (e) => [ + new paper.Path.RegularPolygon({ + center: e.coor, + sides: 12, + radius: R, + data: { mer: 1 }, + }).rotate(15), + ...Array.from({ length: 2 }, (_, i) => + new paper.Path.RegularPolygon({ + center: e.coor.add([0, r12 + h3_12]), + sides: 6, + radius: a12, + data: { mer: 2 }, + }) + .rotate(-30) + .rotate(-60 * i, e.coor) + ), + ...Array.from({ length: 3 }, (_, i) => + new paper.Path.RegularPolygon({ + center: e.coor.add([0, r12 + a12 / 2]), + sides: 4, + radius: (SQRT2 * a12) / 2, + data: { mer: 3 }, + }).rotate(-30 - 60 * i, e.coor) + ), + ], + radius: R + 2 * h3_12, + }; + } else if (t === "bisectedhex") { + tile = { + basis: [ + [2 * r, 0], + [r, SQRT3 * r], + ], + tile: (e) => + Array.from({ length: 6 }, (_, i) => { + const result = new paper.Path.RegularPolygon({ + center: e.coor.add([0, r - (R * SQRT3) / 6]), + sides: 3, + radius: R / SQRT3, + }) + .rotate(30 + i * 60, e.coor) + .divide(new paper.Path.RegularPolygon({ center: e.coor, sides: 3, radius: R })); + result.remove(); + result.children.forEach((f) => (f.data = { mer: 1 })); + console.log(result.children); + return result.children.map((f) => f.clone()); + }).flat(), + radius: R, + }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0e4a692..cd1d869 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "democapsid", - "version": "2.2.1", + "version": "2.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "democapsid", - "version": "2.2.1", + "version": "2.2.2", "license": "MIT", "dependencies": { "paper": "^0.12.18" diff --git a/package.json b/package.json index c8292fd..00f92ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "democapsid", - "version": "2.2.1", + "version": "2.2.2", "author": "Daniel Antonio Negrón", "license": "MIT", "description": "Render viral capsids in the browser and export SVG.", diff --git a/paper/apa.csl b/paper/apa.csl deleted file mode 100644 index 32be69e..0000000 --- a/paper/apa.csl +++ /dev/null @@ -1,443 +0,0 @@ - - diff --git a/paper/figure.pdf b/paper/figure.pdf deleted file mode 100644 index 1b13d42..0000000 Binary files a/paper/figure.pdf and /dev/null differ diff --git a/paper/figure.pptx b/paper/figure.pptx deleted file mode 100644 index ebbd3df..0000000 Binary files a/paper/figure.pptx and /dev/null differ diff --git a/paper/paper.Rmd b/paper/paper.Rmd deleted file mode 100644 index 8a05c61..0000000 --- a/paper/paper.Rmd +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: 'Vectorized capsid rendering in the browser with capsid.js' -tags: - - virus - - capsid - - caspar-klug - - icosahedron - - hexamer - - pentamer - - rendering - - javascript - - svg - - net - - 2D - - 3D -authors: - - name: Daniel Antonio Negrón - orcid: 0000-0002-6123-2441 - affiliation: 1 -affiliations: - - name: George Mason University, Bioinformatics and Computational Biology, 10900 University Blvd, Manassas, VA, 20110 - index: 1 -citation_author: Negrón -date: 11 January 2022 -year: 2022 -bibliography: paper.bib -output: rticles::joss_article -csl: apa.csl -journal: JOSS ---- - -# Summary - -Online viral resources lack high-quality capsid models for designing infographics and scientific figures. Accordingly, the capsid.js library implements Caspar-Klug theory to generate SVG files compatible with office and design software. The corresponding online application parameterizes style options, perspectives, and specialized lattice patterns. This project is actively developed on GitHub (https://github.com/dnanto/capsid), distributed under the MIT License, and hosted on GitHub Pages (https://dnanto.github.io/capsid/capsid.html). Supplementary data are available on GitHub. - - -# Statement of need - -Molecular biology is the study of the building blocks of life and how they organize into complex objects via intricate processes and cycles. Researchers, professors, and authors in the field often condense complicated concepts into simplified diagrams and cartoons for effective communication. Such representations are useful for describing viral capsid structure, assembly, and interactions. - -The ViralZone web resource takes a direct approach, providing a static reference diagram for each known virus [@huloViralZoneKnowledgeResource2011]. This contrasts with dynamic computational resources such as VIPER, which assembles realistic capsid structures from PDB files [@reddyVirusParticleExplorer2001]. VIPERdb hosts the Icosahedral Server, but it only exports three-dimensional files in the same specialized format [@carrillo-trippVIPERdb2EnhancedWeb2009]. Neither resource provides an interactive online method for generating simple, two and three-dimensional structure files that are compatible with office or vector graphics software for editing. - -To solve this problem, the capsid.js project provides an interactive tool that renders icosahedral capsids and their nets in the browser. It parameterizes styling, projections, and lattice geometries and exports resulting models to SVG, since they are publication-quality, infinitely scalable, and compatible with word processors and vector graphics editors. This also means that the user is free to remix the SVG shape components. - - -# Methods - -The application consists of an object-oriented JavaScript library that implements Caspar-Klug theory to generate and project the faces of the unit icosahedron [@casparPhysicalPrinciplesConstruction1962]. It also includes the Paper.js library for 2D vector graphics methods [@lehniPaperjsPaperJs2020]. A simple linear algebra engine provides methods to compute a camera matrix and project points. Figure 1 combines output from the program to describe the construction process. - -![Construct the face and icosahedron for a levo T7 viral capsid ($T=h^2+hk+k^2=2^2+(2)(1)+1^2$). This figure shows how office software such as Microsoft PowerPoint can import and convert the SVG files generated with capsid.js into shape objects. (A) Select the unit tile (hexagon) and draw a grid. Draw a triangle over the grid, moving $h$ and $k$ tile units forwards and left respectively. (B) Extract the triangular area from the grid. Color the hexamers and pentamers based on the unit tile circumradius from each face vertex. (C) Project the face to the 2D coordinates of the icosahedron based on the camera matrix. (D) Alternate tiling patterns. (E) The icosahedral net.](figure.pdf "Figure 1") - -## Face - -The first rendering step generates a grid of the selected tile geometry, including hexagonal, trihexagonal, snub hexagonal, rhombitrihexagonal, or the dual projection of any of the former. These additional tiles account for viruses with specialized lattice architectures [@twarockStructuralPuzzlesVirology2019]. Next, the procedure draws an equilateral triangle based on the $h$ and $k$ parameters, moving $h$ tile units forward and $k$ to the left or right depending on the selected levo or dextro parameter. This walk generates a triangle that serves as a cookie-cutter for removing the portion of the grid that serves as the face. - -## Color - -The application includes color options for the hexamer, pentamer, fiber, and fiber knob domain. Additional colors correspond to the selected tiling. Disambiguation between hexamer and pentamer depends on whether the tile occurs at a vertex based on the grid walk. - -## Projection - -To project each face, the procedure computes the unit icosahedron and applies the camera matrix to each vertex. Sorting each face by the z-coordinates achieves realistic occlusion. An affine transform operation then translates each face to the corresponding 2D projection coordinates of each face on the solid. To render vertex proteins, such as the adenovirus fiber, the procedure computes the vertexes of a larger icosahedron and includes the corresponding objects in the z-ordering operation. - -## Discussion - -The capsid.js app renders customizable icosahedral viral capsids and was recently used to model the adeno-associated virus (AAV) [@hamannImprovedTargetingHuman2021]. Development is active with plans to include elongated (prolate/oblate) capsids. A separate prototype for generating elongated capsid nets is available based on theory [@moodyShapeTevenBacteriophage1965; @luqueStructureElongatedViral2010]. High-resolution SVG continues being a primary design goal to aid in the creation of detailed figures. This includes rendering all details as separate shapes. As a result, the current implementation may lag for large values of $h$ or $k$. However, performance enhancements are possible by improving the rendering algorithm or switching to a GPU-based library, such as WebGL. - - -# Acknowledgements - -The author would like the thank his dissertation committee: Dr. Donald Seto, Dr. Patrick Gillevet, and Dr. Sterling Thomas. Also, the author would like to thank Shane Mitchell, Mychal Ivancich, and Mitchell Holland for comments and review. This constitutes a portion (Chapter 2) of the PhD dissertation "Molecular Clock Analysis of Human Adenovirus" submitted to GMU. - - -# References diff --git a/paper/paper.bib b/paper/paper.bib deleted file mode 100644 index 18df501..0000000 --- a/paper/paper.bib +++ /dev/null @@ -1,141 +0,0 @@ -@Article{huloViralZoneKnowledgeResource2011, - title = {{{ViralZone}}: A Knowledge Resource to Understand Virus Diversity}, - shorttitle = {{{ViralZone}}}, - author = {Chantal Hulo and Edouard {de Castro} and Patrick Masson and Lydie Bougueleret and Amos Bairoch and Ioannis Xenarios and Philippe {Le Mercier}}, - year = {2011}, - month = {jan}, - journal = {Nucleic Acids Research}, - volume = {39}, - number = {suppl\_1}, - pages = {D576-D582}, - issn = {0305-1048, 1362-4962}, - doi = {10.1093/nar/gkq901}, - langid = {english}, - file = {/Users/dnanto/Zotero/storage/RHYEI5YU/Hulo et al. - 2011 - ViralZone a knowledge resource to understand viru.pdf}, -} -@Article{reddyVirusParticleExplorer2001, - title = {Virus {{Particle Explorer}} ({{VIPER}}), a {{Website}} for {{Virus Capsid Structures}} and {{Their Computational Analyses}}}, - author = {Vijay S. Reddy and Padmaja Natarajan and Brian Okerberg and Kevin Li and K. V. Damodaran and Ryan T. Morton and Charles L. Brooks and John E. Johnson}, - year = {2001}, - month = {dec}, - journal = {Journal of Virology}, - volume = {75}, - number = {24}, - pages = {11943--11947}, - publisher = {{American Society for Microbiology Journals}}, - issn = {0022-538X, 1098-5514}, - doi = {10.1128/JVI.75.24.11943-11947.2001}, - abstract = {The number of icosahedral-capsid structures determined at a near-atomic level of resolution is growing rapidly as advances in synchrotron radiation sources, fast-readout detectors, and computer hardware and software are made. Hence, there is an increasing need to organize these mega-assemblies into}, - chapter = {GUEST COMMENTARY}, - copyright = {Copyright \textcopyright{} 2001 American Society for Microbiology}, - langid = {english}, - pmid = {11711584}, - file = {/Users/dnanto/Zotero/storage/VICKPA7S/Reddy et al. - 2001 - Virus Particle Explorer (VIPER), a Website for Vir.pdf;/Users/dnanto/Zotero/storage/MIR6Q38D/11943.html}, -} -@Article{carrillo-trippVIPERdb2EnhancedWeb2009, - title = {{{VIPERdb2}}: An Enhanced and Web {{API}} Enabled Relational Database for Structural Virology}, - shorttitle = {{{VIPERdb2}}}, - author = {M. Carrillo-Tripp and C. M. Shepherd and I. A. Borelli and S. Venkataraman and G. Lander and P. Natarajan and J. E. Johnson and C. L. Brooks and V. S. Reddy}, - year = {2009}, - month = {jan}, - journal = {Nucleic Acids Research}, - volume = {37}, - number = {Database}, - pages = {D436-D442}, - issn = {0305-1048, 1362-4962}, - doi = {10.1093/nar/gkn840}, - langid = {english}, - file = {/Users/dnanto/Zotero/storage/L7L63VXC/Carrillo-Tripp et al. - 2009 - VIPERdb2 an enhanced and web API enabled relation.pdf}, -} -@Article{casparPhysicalPrinciplesConstruction1962, - title = {Physical Principles in the Construction of Regular Viruses}, - author = {D. L. Caspar and A. Klug}, - year = {1962}, - journal = {Cold Spring Harbor Symposia on Quantitative Biology}, - volume = {27}, - pages = {1--24}, - issn = {0091-7451}, - doi = {10.1101/sqb.1962.027.001.005}, - langid = {english}, - pmid = {14019094}, - keywords = {DNA,DNA; Viral,DNA; VIRAL,RNA,RNA; Viral,RNA; VIRAL,Viruses,VIRUSES}, - file = {/Users/dnanto/Zotero/storage/7TXVYTE4/Caspar and Klug - 1962 - Physical principles in the construction of regular.pdf}, -} -@Misc{lehniPaperjsPaperJs2020, - title = {Paperjs/Paper.Js}, - author = {J{\"u}rg Lehni and Jonathan Puckey}, - year = {2020}, - month = {nov}, - abstract = {The Swiss Army Knife of Vector Graphics Scripting \textendash{} Scriptographer ported to JavaScript and the browser, using HTML5 Canvas. Created by @lehni \& @puckey}, - copyright = {View license , View license}, - howpublished = {Paper.js}, - publisher = {GitHub}, - journal = {GitHub repository}, - url = {https://github.com/paperjs/paper.js} -} -@Article{twarockStructuralPuzzlesVirology2019, - title = {Structural Puzzles in Virology Solved with an Overarching Icosahedral Design Principle}, - author = {Reidun Twarock and Antoni Luque}, - year = {2019}, - month = {sep}, - journal = {Nature Communications}, - volume = {10}, - number = {1}, - pages = {4414}, - publisher = {{Nature Publishing Group}}, - issn = {2041-1723}, - doi = {10.1038/s41467-019-12367-3}, - abstract = {Viruses have evolved protein containers with a wide spectrum of icosahedral architectures to protect their genetic material. The geometric constraints defining these container designs, and their implications for viral evolution, are open problems in virology. The principle of quasi-equivalence is currently used to predict virus architecture, but improved imaging techniques have revealed increasing numbers of viral outliers. We show that this theory is a special case of an overarching design principle for icosahedral, as well as octahedral, architectures that can be formulated in terms of the Archimedean lattices and their duals. These surface structures encompass different blueprints for capsids with the~same number of structural proteins, as well as for capsid architectures formed from a combination of minor and major capsid proteins, and are recurrent within viral lineages. They also apply to other icosahedral structures in nature, and offer alternative designs for man-made materials and nanocontainers in bionanotechnology.}, - copyright = {2019 The Author(s)}, - langid = {english}, - file = {/Users/dnanto/Zotero/storage/MU9W3TNL/Twarock and Luque - 2019 - Structural puzzles in virology solved with an over.pdf;/Users/dnanto/Zotero/storage/SRAW8LVK/41467_2019_12367_MOESM1_ESM.pdf;/Users/dnanto/Zotero/storage/7BWD8KSE/s41467-019-12367-3.html}, -} -@Article{luqueStructureElongatedViral2010, - title = {The {{Structure}} of {{Elongated Viral Capsids}}}, - author = {Antoni Luque and David Reguera}, - year = {2010}, - month = {jun}, - journal = {Biophysical Journal}, - volume = {98}, - number = {12}, - pages = {2993--3003}, - issn = {0006-3495}, - doi = {10.1016/j.bpj.2010.02.051}, - abstract = {There are many viruses whose genetic material is protected by a closed elongated protein shell. Unlike spherical viruses, the structure and construction principles of these elongated capsids are not fully known. In this article, we have developed a general geometrical model to describe the structure of prolate or bacilliform capsids. We show that only a limited set of tubular architectures can be built closed by hemispherical icosahedral caps. In particular, the length and number of proteins adopt a very special set of discrete values dictated by the axial symmetry (fivefold, threefold, or twofold) and the triangulation number of the caps. The results are supported by experimental observations and simulations of simplified physical models. This work brings about a general classification of elongated viruses that will help to predict their structure, and to design viral cages with tailored geometrical properties for biomedical and nanotechnological applications.}, - pmcid = {PMC2884239}, - pmid = {20550912}, - file = {/Users/dnanto/Zotero/storage/ARU2VUIZ/Luque and Reguera - 2010 - The Structure of Elongated Viral Capsids.pdf;/Users/dnanto/Zotero/storage/VY4RQ98U/mmc1.pdf}, -} - -@Article{moodyShapeTevenBacteriophage1965, - title = {The Shape of the {{T-even}} Bacteriophage Head}, - author = {M. F. Moody}, - year = {1965}, - month = {aug}, - journal = {Virology}, - volume = {26}, - number = {4}, - pages = {567--576}, - issn = {0042-6822}, - doi = {10.1016/0042-6822(65)90319-3}, - abstract = {Electron micrographs of T-even heads have provided no valid reason for believing that the head shape must be a bipyramidal hexagonal prism, as other polyhedra also give hexagonal profiles similar to those shown by T-even heads. Comparison of the structure of the normal head with various defective head forms shows that (1) the molecules are probably hexagonally packed in the head membrane and (2) the local packing of head protein molecules is the same round a terminal and a nonterminal vertex. Of four hypotheses which are described, only two are consistent with this evidence; both require the normal head to have the structure of an isometric capsid with icosahedral symmetry, in which the middle portion has been extended. Of these two hypotheses, the head shape is most satisfactorily accounted for by a hypothesis according to which the normal head has 52 symmetry and has a shape derived from a regular icosahedron by the extension of its middle section (`prolate icosahedron'). It is shown that it is possible to accommodate the 5-fold axis of this head with the 6-fold symmetry of the base plate.}, - langid = {english}, - file = {/Users/dnanto/Zotero/storage/IXL5PJRD/Moody - 1965 - The shape of the T-even bacteriophage head.pdf;/Users/dnanto/Zotero/storage/9CAJTX6T/0042682265903193.html}, -} -@Article{hamannImprovedTargetingHuman2021, - title = {Improved Targeting of Human {{CD4}}+ {{T}} Cells by Nanobody-Modified {{AAV2}} Gene Therapy Vectors}, - author = {Martin V. Hamann and Niklas Beschorner and Xuan-Khang Vu and Ilona Hauber and Ulrike C. Lange and Bjoern Traenkle and Philipp D. Kaiser and Daniel Foth and Carola Schneider and Hildegard B{\"u}ning and Ulrich Rothbauer and Joachim Hauber}, - year = {2021}, - month = {dec}, - journal = {PLOS ONE}, - volume = {16}, - number = {12}, - pages = {e0261269}, - publisher = {{Public Library of Science}}, - issn = {1932-6203}, - doi = {10.1371/journal.pone.0261269}, - abstract = {Adeno-associated viruses (AAV) are considered non-pathogenic in humans, and thus have been developed into powerful vector platforms for in vivo gene therapy. Although the various AAV serotypes display broad tropism, frequently infecting multiple tissues and cell types, vectors for specific and efficient targeting of human CD4+ T lymphocytes are largely missing. In fact, a substantial translational bottleneck exists in the field of therapeutic gene transfer that would require in vivo delivery into peripheral disease-related lymphocytes for subsequent genome editing. To solve this issue, capsid modification for retargeting AAV tropism, and in turn improving vector potency, is considered a promising strategy. Here, we genetically modified the minor AAV2 capsid proteins, VP1 and VP2, with a set of novel nanobodies with high-affinity for the human CD4 receptor. These novel vector variants demonstrated improved targeting of human CD4+ cells, including primary human peripheral blood mononuclear cells (PBMC) and purified human CD4+ T lymphocytes. Thus, the technical approach presented here provides a promising strategy for developing specific gene therapy vectors, particularly targeting disease-related peripheral blood CD4+ leukocytes.}, - langid = {english}, - keywords = {Capsids,Flow cytometry,Gene therapy,Genomics,Lymphocytes,Oligonucleotides,Polymerase chain reaction,T helper cells}, - file = {/Users/dnanto/Zotero/storage/SLPA89LF/Hamann et al. - 2021 - Improved targeting of human CD4+ T cells by nanobo.pdf;/Users/dnanto/Zotero/storage/S9SEUEAY/article.html}, -} diff --git a/paper/paper.md b/paper/paper.md deleted file mode 100644 index 8a05c61..0000000 --- a/paper/paper.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: 'Vectorized capsid rendering in the browser with capsid.js' -tags: - - virus - - capsid - - caspar-klug - - icosahedron - - hexamer - - pentamer - - rendering - - javascript - - svg - - net - - 2D - - 3D -authors: - - name: Daniel Antonio Negrón - orcid: 0000-0002-6123-2441 - affiliation: 1 -affiliations: - - name: George Mason University, Bioinformatics and Computational Biology, 10900 University Blvd, Manassas, VA, 20110 - index: 1 -citation_author: Negrón -date: 11 January 2022 -year: 2022 -bibliography: paper.bib -output: rticles::joss_article -csl: apa.csl -journal: JOSS ---- - -# Summary - -Online viral resources lack high-quality capsid models for designing infographics and scientific figures. Accordingly, the capsid.js library implements Caspar-Klug theory to generate SVG files compatible with office and design software. The corresponding online application parameterizes style options, perspectives, and specialized lattice patterns. This project is actively developed on GitHub (https://github.com/dnanto/capsid), distributed under the MIT License, and hosted on GitHub Pages (https://dnanto.github.io/capsid/capsid.html). Supplementary data are available on GitHub. - - -# Statement of need - -Molecular biology is the study of the building blocks of life and how they organize into complex objects via intricate processes and cycles. Researchers, professors, and authors in the field often condense complicated concepts into simplified diagrams and cartoons for effective communication. Such representations are useful for describing viral capsid structure, assembly, and interactions. - -The ViralZone web resource takes a direct approach, providing a static reference diagram for each known virus [@huloViralZoneKnowledgeResource2011]. This contrasts with dynamic computational resources such as VIPER, which assembles realistic capsid structures from PDB files [@reddyVirusParticleExplorer2001]. VIPERdb hosts the Icosahedral Server, but it only exports three-dimensional files in the same specialized format [@carrillo-trippVIPERdb2EnhancedWeb2009]. Neither resource provides an interactive online method for generating simple, two and three-dimensional structure files that are compatible with office or vector graphics software for editing. - -To solve this problem, the capsid.js project provides an interactive tool that renders icosahedral capsids and their nets in the browser. It parameterizes styling, projections, and lattice geometries and exports resulting models to SVG, since they are publication-quality, infinitely scalable, and compatible with word processors and vector graphics editors. This also means that the user is free to remix the SVG shape components. - - -# Methods - -The application consists of an object-oriented JavaScript library that implements Caspar-Klug theory to generate and project the faces of the unit icosahedron [@casparPhysicalPrinciplesConstruction1962]. It also includes the Paper.js library for 2D vector graphics methods [@lehniPaperjsPaperJs2020]. A simple linear algebra engine provides methods to compute a camera matrix and project points. Figure 1 combines output from the program to describe the construction process. - -![Construct the face and icosahedron for a levo T7 viral capsid ($T=h^2+hk+k^2=2^2+(2)(1)+1^2$). This figure shows how office software such as Microsoft PowerPoint can import and convert the SVG files generated with capsid.js into shape objects. (A) Select the unit tile (hexagon) and draw a grid. Draw a triangle over the grid, moving $h$ and $k$ tile units forwards and left respectively. (B) Extract the triangular area from the grid. Color the hexamers and pentamers based on the unit tile circumradius from each face vertex. (C) Project the face to the 2D coordinates of the icosahedron based on the camera matrix. (D) Alternate tiling patterns. (E) The icosahedral net.](figure.pdf "Figure 1") - -## Face - -The first rendering step generates a grid of the selected tile geometry, including hexagonal, trihexagonal, snub hexagonal, rhombitrihexagonal, or the dual projection of any of the former. These additional tiles account for viruses with specialized lattice architectures [@twarockStructuralPuzzlesVirology2019]. Next, the procedure draws an equilateral triangle based on the $h$ and $k$ parameters, moving $h$ tile units forward and $k$ to the left or right depending on the selected levo or dextro parameter. This walk generates a triangle that serves as a cookie-cutter for removing the portion of the grid that serves as the face. - -## Color - -The application includes color options for the hexamer, pentamer, fiber, and fiber knob domain. Additional colors correspond to the selected tiling. Disambiguation between hexamer and pentamer depends on whether the tile occurs at a vertex based on the grid walk. - -## Projection - -To project each face, the procedure computes the unit icosahedron and applies the camera matrix to each vertex. Sorting each face by the z-coordinates achieves realistic occlusion. An affine transform operation then translates each face to the corresponding 2D projection coordinates of each face on the solid. To render vertex proteins, such as the adenovirus fiber, the procedure computes the vertexes of a larger icosahedron and includes the corresponding objects in the z-ordering operation. - -## Discussion - -The capsid.js app renders customizable icosahedral viral capsids and was recently used to model the adeno-associated virus (AAV) [@hamannImprovedTargetingHuman2021]. Development is active with plans to include elongated (prolate/oblate) capsids. A separate prototype for generating elongated capsid nets is available based on theory [@moodyShapeTevenBacteriophage1965; @luqueStructureElongatedViral2010]. High-resolution SVG continues being a primary design goal to aid in the creation of detailed figures. This includes rendering all details as separate shapes. As a result, the current implementation may lag for large values of $h$ or $k$. However, performance enhancements are possible by improving the rendering algorithm or switching to a GPU-based library, such as WebGL. - - -# Acknowledgements - -The author would like the thank his dissertation committee: Dr. Donald Seto, Dr. Patrick Gillevet, and Dr. Sterling Thomas. Also, the author would like to thank Shane Mitchell, Mychal Ivancich, and Mitchell Holland for comments and review. This constitutes a portion (Chapter 2) of the PhD dissertation "Molecular Clock Analysis of Human Adenovirus" submitted to GMU. - - -# References