mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 18:27:02 -06:00
Pen cursors: a fountain-pen nib on the selection arrow's axis
The old pen glyph was a crude pentagon with a floating dot and a stray stem. Redraw it as the precision instrument it is: the pen-tool icon's nib — sharp point on the hotspot, shoulders flaring to their widest two-thirds back, a narrower flat heel, a slit widening into the round breather hole — swung round onto the selection arrow's midline (arrow_axis), so holding Cmd/Ctrl to swap between the Pen and the selection tools no longer rotates the cursor under the pointer. Badges keep their meanings (*, +, -, o, /, j, ^, ~) and multi-stroke ones now draw all halos under all ink via strokes(), so neighbouring strokes no longer cut each other into blobs.
This commit is contained in:
@@ -146,7 +146,7 @@ shown_when = [
|
||||
"With Use Precise Cursors on, the crosshair shows instead.",
|
||||
]
|
||||
hotspot = "The nib's tip."
|
||||
look = "A pen nib. Every Pen-family cursor is this nib with a different badge."
|
||||
look = "A fountain-pen nib pointing up-left along the selection arrow's axis (so swapping to the selection tools doesn't rotate the cursor), its slit widening into the breather hole and its heel clipped at the back; the shape approximates the pen tool's icon. Every Pen-family cursor is this nib with a different badge."
|
||||
|
||||
[[cursor]]
|
||||
id = "PenDrawing"
|
||||
|
||||
@@ -77,22 +77,37 @@ fn eyedropper(p: &mut Ink, o: Pos2) {
|
||||
line(p, o + vec2(11.0, -19.0), o + vec2(19.0, -11.0));
|
||||
}
|
||||
|
||||
/// The selection arrow's axis: the midline between its two edges at the tip, a unit vector.
|
||||
/// The pen nib lies along it, so swapping between the Pen and the selection tools (Cmd/Ctrl)
|
||||
/// doesn't rotate the cursor.
|
||||
fn arrow_axis() -> Vec2 {
|
||||
let a = arrow_points(Pos2::ZERO);
|
||||
let (e1, e2) = (a[1] - a[0], a[6] - a[0]);
|
||||
(e1.normalized() + e2.normalized()).normalized()
|
||||
}
|
||||
|
||||
/// The Pen family's nib: the pen-tool icon's, swung round onto the selection arrow's axis
|
||||
/// (`arrow_axis`, down-right) so swapping to the selection tools doesn't rotate the cursor, its
|
||||
/// point on the hotspot `o`. In nib coordinates `u` runs along the axis and `v` across it: the
|
||||
/// silhouette is symmetric about the axis — the point, shoulders flaring to their widest
|
||||
/// two-thirds back, then a narrower flat heel. The slit runs back along the axis from the point,
|
||||
/// widening into the round breather hole; both are cut to the halo's white. Every Pen cursor is
|
||||
/// this nib plus a badge below right of it saying what a click does.
|
||||
fn pen(p: &mut Ink, o: Pos2, badge: &str) {
|
||||
// Nib pointing to the top-left hotspot.
|
||||
let pts = vec![o, o + vec2(4.0, 12.0), o + vec2(8.0, 16.0), o + vec2(16.0, 8.0), o + vec2(12.0, 4.0)];
|
||||
poly(p, pts, INK, INK);
|
||||
p.circle_filled(o + vec2(6.0, 6.0), 1.4, HALO);
|
||||
line(p, o + vec2(10.0, 14.0), o + vec2(14.0, 18.0));
|
||||
let b = o + vec2(16.0, 14.0);
|
||||
let d = arrow_axis();
|
||||
let n = vec2(-d.y, d.x);
|
||||
let at = |u: f32, v: f32| o + u * d + v * n;
|
||||
poly(p, vec![at(0.0, 0.0), at(10.5, -6.3), at(15.5, -3.0), at(15.5, 3.0), at(10.5, 6.3)], INK, INK);
|
||||
// The slit is a wedge opening at the point, widening into the round breather hole.
|
||||
p.add(Shape::convex_polygon(vec![at(2.0, 0.0), at(10.4, -0.85), at(10.4, 0.85)], HALO, Stroke::NONE));
|
||||
p.circle_filled(at(11.8, 0.0), 1.7, HALO);
|
||||
let b = o + vec2(13.0, 12.0);
|
||||
match badge {
|
||||
"o" => {
|
||||
p.circle_stroke(b + vec2(3.0, 3.0), 3.0, Stroke::new(3.0, HALO));
|
||||
p.circle_stroke(b + vec2(3.0, 3.0), 3.0, Stroke::new(1.2, INK));
|
||||
}
|
||||
"+" => {
|
||||
line(p, b + vec2(0.0, 3.0), b + vec2(6.0, 3.0));
|
||||
line(p, b + vec2(3.0, 0.0), b + vec2(3.0, 6.0));
|
||||
}
|
||||
"+" => strokes(p, &[vec![b + vec2(0.0, 3.0), b + vec2(6.0, 3.0)], vec![b + vec2(3.0, 0.0), b + vec2(3.0, 6.0)]]),
|
||||
"-" => line(p, b + vec2(0.0, 3.0), b + vec2(6.0, 3.0)),
|
||||
"/" => line(p, b + vec2(0.0, 6.0), b + vec2(5.0, 0.0)),
|
||||
"~" => {
|
||||
@@ -108,14 +123,18 @@ fn pen(p: &mut Ink, o: Pos2, badge: &str) {
|
||||
let sq = [(3.0, -1.0), (7.0, -1.0), (7.0, 3.0), (3.0, 3.0)].map(|(x, y)| b + vec2(x, y));
|
||||
poly(p, sq.to_vec(), INK, INK);
|
||||
}
|
||||
"^" => {
|
||||
line(p, b + vec2(0.0, 6.0), b + vec2(3.0, 0.0));
|
||||
line(p, b + vec2(3.0, 0.0), b + vec2(6.0, 6.0));
|
||||
}
|
||||
"^" => strokes(p, &[vec![b + vec2(0.0, 6.0), b + vec2(3.0, 0.0)], vec![b + vec2(3.0, 0.0), b + vec2(6.0, 6.0)]]),
|
||||
"*" => {
|
||||
line(p, b + vec2(0.0, 0.0), b + vec2(6.0, 6.0));
|
||||
line(p, b + vec2(6.0, 0.0), b + vec2(0.0, 6.0));
|
||||
line(p, b + vec2(3.0, -1.0), b + vec2(3.0, 7.0));
|
||||
// The new-path star: eight spokes on the badge's centre.
|
||||
strokes(
|
||||
p,
|
||||
&[
|
||||
vec![b + vec2(0.6, 0.6), b + vec2(5.4, 5.4)],
|
||||
vec![b + vec2(5.4, 0.6), b + vec2(0.6, 5.4)],
|
||||
vec![b + vec2(3.0, 0.0), b + vec2(3.0, 6.0)],
|
||||
vec![b + vec2(0.0, 3.0), b + vec2(6.0, 3.0)],
|
||||
],
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -841,16 +860,45 @@ mod tests {
|
||||
assert!(body[3] == 255 && body[..3].iter().all(|&c| c < 40), "{body:?}");
|
||||
}
|
||||
|
||||
/// The pen nib is a fountain pen's lying along the selection arrow's axis: its point on the
|
||||
/// hotspot, the slit running back along it into the round breather hole, the heel clipped
|
||||
/// at the back.
|
||||
#[test]
|
||||
fn the_pen_nib_has_a_slit_and_a_breather_hole() {
|
||||
for ppp in [1.0f32, 2.0] {
|
||||
let img = rasterize(Cursor::PenDrawing, ppp).expect("a bitmap");
|
||||
let [hx, hy] = img.hotspot.map(usize::from);
|
||||
let at = |x: f32, y: f32| {
|
||||
let (px, py) = ((hx as f32 + x * ppp) as usize, (hy as f32 + y * ppp) as usize);
|
||||
if px < img.size[0] as usize && py < img.size[1] as usize { pixel(&img, px, py) } else { [0; 4] }
|
||||
};
|
||||
// The breather hole, ~12 points along the axis ((4.6, 10.9) on screen): fully white.
|
||||
let hole = at(4.6, 10.9);
|
||||
assert!(hole[3] > 200 && hole[0] > 192, "the breather hole at {ppp}: {hole:?}");
|
||||
// The slit on the way to the hole: white over a black field.
|
||||
let slit = at(2.4, 5.5);
|
||||
assert!(slit[3] > 40 && slit[0] > 128, "the slit at {ppp}: {slit:?}");
|
||||
// The body past the hole is solid ink.
|
||||
let body = at(5.5, 12.9);
|
||||
assert!(body[3] > 200 && body[0] < 64, "the nib's body at {ppp}: {body:?}");
|
||||
// The heel's corners carry ink out near the bitmap's edge.
|
||||
for (x, y) in [(8.8f32, 13.1), (3.3, 15.4)] {
|
||||
let corner = at(x, y);
|
||||
assert!(corner[3] > 0, "a heel corner at {ppp} ({x}, {y}): {corner:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Pen drawing a path is the same nib as the new-path Pen, without the asterisk.
|
||||
#[test]
|
||||
fn the_drawing_pen_has_no_new_path_asterisk() {
|
||||
for ppp in [1.0, 2.0] {
|
||||
let (new, drawing) = (rasterize(Cursor::Pen, ppp).unwrap(), rasterize(Cursor::PenDrawing, ppp).unwrap());
|
||||
assert_eq!(new.hotspot, drawing.hotspot, "at {ppp}");
|
||||
// The asterisk's center, 19 × 17 points from the hotspot.
|
||||
// The asterisk's center, 16 × 15 points from the hotspot.
|
||||
let at = |img: &CustomCursorImage| {
|
||||
let [hx, hy] = img.hotspot.map(usize::from);
|
||||
let (x, y) = (hx + (19.0 * ppp) as usize, hy + (17.0 * ppp) as usize);
|
||||
let (x, y) = (hx + (16.0 * ppp) as usize, hy + (15.0 * ppp) as usize);
|
||||
if x < img.size[0] as usize && y < img.size[1] as usize { pixel(img, x, y)[3] } else { 0 }
|
||||
};
|
||||
assert_eq!(at(&new), 255, "the new-path Pen has its asterisk at {ppp}");
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Click an open path's end to continue it; click another path's end to join | done | #776 |
|
||||
| Rubber Band preview | done | Preferences option |
|
||||
| Auto Add/Delete on a selected path; Shift or the preference disables it | done | |
|
||||
| Pen cursors (new path ×, continue /, close o, add +, delete −, convert ^) | partial | vector cursors exist; not every state verified |
|
||||
| Pen cursors (new path ×, continue /, close o, add +, delete −, convert ^) | done | the nib is a fountain-pen nib, tip on the hotspot, with the slit widening into the breather hole and the heel clipped square to its axis; bitmap tests at 1× and 2× pin the nib and the new-path asterisk, and in-app screenshots compare the Pen with Direct Selection |
|
||||
| Node-editing cursors: Direct Selection's white arrow with a hollow or filled square over an anchor (path unselected or selected); the Reshape Segment cursor over a curved segment (Direct Selection, Anchor Point, the Pen with Alt); the Anchor Point caret for the Pen with Alt; Group Selection's plus (also Direct Selection with Alt); a filled arrow while dragging that turns hollow on a snap; a double arrowhead for an Alt-drag copy | partial | as the public manual describes them, drawn in code; no cue over handles (none documented) |
|
||||
| Anchor Point and Curvature tools' own cursors | done | the caret on the anchor it converts; the pen nib with a curve badge |
|
||||
| Free Transform's Cmd/Ctrl side-shear cursor | done | two arrows passing along the side (ShearH, ShearV), instead of the resize arrow across it |
|
||||
|
||||
Reference in New Issue
Block a user