mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 12:47:13 -06:00
Flare: the drag turns the rays, Shift keeps their orientation and Space moves the flare
The reference app's Flare tool rotates the rays as the center is dragged out, Shift constrains them to their original orientation, and Space held moves the shape being drawn. VectorCraft's Flare tool drew the rays in one fixed orientation and ignored Shift and Space. shape.flare takes rotation (degrees, clockwise), which turns the rays about the center and leaves the halo, the center and the rings as they are. While the center is dragged out, the rays turn with the pointer, as a polygon's first corner does, and a drag straight up leaves them unturned. Shift keeps them unturned, or turned by General › Constrain Angle as a polygon drawn with Shift is. Space held during the center drag or the drag that places the rings' end point moves the flare at its current size, and the drag goes on from there when Space is let go, as with the shape tools. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -29,7 +29,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Flare",
|
||||
[],
|
||||
None,
|
||||
"{cx, cy, diameter=100, opacity=50 (%), brightness=30 (%), growth=20 (%), fuzziness=50 (%), rays=15, longest=300 (%), rayFuzziness=100 (%), x2?, y2? (ring end; else pathLength at direction), pathLength=300, rings=10, largest=50 (%), direction=45 (deg), seed?} → {id}",
|
||||
"{cx, cy, diameter=100, opacity=50 (%), brightness=30 (%), growth=20 (%), fuzziness=50 (%), rays=15, longest=300 (%), rayFuzziness=100 (%), rotation=0 (deg, turns the rays clockwise), x2?, y2? (ring end; else pathLength at direction), pathLength=300, rings=10, largest=50 (%), direction=45 (deg), seed?} → {id}",
|
||||
has_doc,
|
||||
flare
|
||||
),
|
||||
@@ -395,6 +395,7 @@ fn flare(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"rays",
|
||||
"longest",
|
||||
"rayFuzziness",
|
||||
"rotation",
|
||||
"rings",
|
||||
"largest",
|
||||
"x2",
|
||||
@@ -413,6 +414,7 @@ fn flare(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let rays = p.get("rays").and_then(Value::as_u64).unwrap_or(15).min(50) as usize;
|
||||
let longest = pct("longest", 300.0);
|
||||
let ray_fuzz = pct("rayFuzziness", 100.0).min(1.0);
|
||||
let ray_turn = f64_or(p, "rotation", 0.0).to_radians();
|
||||
let rings = p.get("rings").and_then(Value::as_u64).unwrap_or(10).min(50) as usize;
|
||||
let largest = pct("largest", 50.0);
|
||||
let end = match (p.get("x2").and_then(Value::as_f64), p.get("y2").and_then(Value::as_f64)) {
|
||||
@@ -443,11 +445,12 @@ fn flare(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let inner = (1.0 - fuzz as f32 * 0.6).clamp(0.05, 0.95);
|
||||
parts.push(("Halo", circle(c, halo_r), radial(c, halo_r, vec![stop(0.0, 0.0), stop(inner * 0.75, 0.0), stop(inner, 0.5), stop(1.0, 0.0)]), 1.0));
|
||||
// Rays: thin spikes of random length (Longest = % of the centre radius), fading outwards.
|
||||
// `rotation` turns them clockwise about the center.
|
||||
if rays > 0 {
|
||||
let mut bp = vectorcraft_geom::BezPath::new();
|
||||
let mut reach: f64 = 0.0;
|
||||
for i in 0..rays {
|
||||
let a = std::f64::consts::TAU * (i as f64 + rnd() * 0.6) / rays as f64;
|
||||
let a = std::f64::consts::TAU * (i as f64 + rnd() * 0.6) / rays as f64 + ray_turn;
|
||||
let len = r * longest * (1.0 - ray_fuzz * 0.75 * rnd()).max(0.1);
|
||||
reach = reach.max(len);
|
||||
let w = r * 0.035;
|
||||
|
||||
@@ -1460,6 +1460,7 @@ fn shape_commands_refuse_a_number_or_boolean_given_as_text() {
|
||||
("shape.flare", with(&flare, "opacity", json!("50")), "opacity"),
|
||||
("shape.flare", with(&flare, "largest", json!(true)), "largest"),
|
||||
("shape.flare", with(&flare, "rays", json!("15")), "rays"),
|
||||
("shape.flare", with(&flare, "rotation", json!("90")), "rotation"),
|
||||
("shape.flare", json!({"cx": 100, "cy": 100, "x2": "300", "y2": 300}), "x2"),
|
||||
("shape.flare", with(&flare, "direction", json!("45")), "direction"),
|
||||
("shape.flare", with(&flare, "seed", json!("7")), "seed"),
|
||||
@@ -3160,6 +3161,33 @@ fn flare_tool_two_step_gesture_is_one_undo() {
|
||||
assert_eq!(rays.path_data().unwrap().subpaths.len(), 13);
|
||||
}
|
||||
|
||||
/// `shape.flare`'s `rotation` turns the rays clockwise about the center, and leaves the halo, the
|
||||
/// center and the rings as they are.
|
||||
#[test]
|
||||
fn flare_rotation_turns_only_the_rays() {
|
||||
use vectorcraft_geom::Affine;
|
||||
let mut s = session();
|
||||
let parts = |s: &mut Session, rotation: f64| {
|
||||
let p = json!({"cx": 200, "cy": 200, "diameter": 60, "rays": 9, "rings": 4, "x2": 400, "y2": 300, "seed": 3, "rotation": rotation});
|
||||
let id = NodeId(s.execute("shape.flare", &p).unwrap()["id"].as_u64().unwrap());
|
||||
let NodeKind::Group { children, .. } = &s.doc().unwrap().doc.node(id).unwrap().kind else { panic!("flare is a group") };
|
||||
children.iter().map(|c| (c.name.clone(), c.path_data().unwrap().clone())).collect::<Vec<_>>()
|
||||
};
|
||||
let plain = parts(&mut s, 0.0);
|
||||
let turned = parts(&mut s, 90.0);
|
||||
assert_eq!(plain.len(), turned.len());
|
||||
let quarter = Affine::translate((200.0, 200.0)) * Affine::rotate(std::f64::consts::FRAC_PI_2) * Affine::translate((-200.0, -200.0));
|
||||
for ((name, a), (_, b)) in plain.iter().zip(&turned) {
|
||||
let pa: Vec<Point> = a.subpaths.iter().flat_map(|sp| sp.anchors.iter().map(|k| k.p)).collect();
|
||||
let pb: Vec<Point> = b.subpaths.iter().flat_map(|sp| sp.anchors.iter().map(|k| k.p)).collect();
|
||||
let want: Vec<Point> = if name.as_deref() == Some("Rays") { pa.iter().map(|&p| quarter * p).collect() } else { pa };
|
||||
assert_eq!(pb.len(), want.len(), "{name:?}");
|
||||
for (p, q) in pb.iter().zip(&want) {
|
||||
assert!(p.distance(*q) < 1e-6, "{name:?}: {p:?} is not {q:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reshape_moves_the_grabbed_point_and_its_neighbourhood() {
|
||||
let mut s = session();
|
||||
|
||||
Reference in New Issue
Block a user