Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/canvas/strokes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,15 @@ export function drawStrokesOnCtx(ctx: CanvasRenderingContext2D, strokes: readonl
// reads as a textured graphite line rather than a flat vector stroke. Pressure = darkness.
if (s.tool === 'pencil') {
const pp = smooth(s.points);
const pw = pp.map((_, i) => baseSize * (0.65 + 0.35 * Math.min(1, prAt(pp, i) * 2)));
// 0.65x at no pressure, 1x at the neutral 0.5, 1.35x at a full press. The `* 2` is
// what puts neutral at 1, and this used to be wrapped in `Math.min(1, ...)` - which
// meant the width stopped growing at 0.5 and every press above it drew exactly the
// same stroke, to the pixel. A stylus writes well above 0.5, so in practice the
// pencil behaved as though pressure were switched off: #338's "pencil pressure is
// ignored". Removing the clamp only extends the curve past neutral; nothing at or
// below 0.5 changes, so no mouse-drawn stroke - which reports no pressure and reads
// as the neutral 0.5 - renders any differently than before.
const pw = pp.map((_, i) => baseSize * (0.65 + 0.7 * prAt(pp, i)));
// As the marker: the scratch, the grain fill and the blit were all full-canvas for
// a stroke of any size. This was the most expensive thing the profiler found.
const box = inkBounds(pp, Math.max(...pw, baseSize) / 2 + SCRATCH_MARGIN, ctx.canvas.width, ctx.canvas.height);
Expand All @@ -491,6 +499,11 @@ export function drawStrokesOnCtx(ctx: CanvasRenderingContext2D, strokes: readonl
// have re-phased the grain per stroke.
tctx.setTransform(1, 0, 0, 1, -box.x, -box.y);
tctx.lineCap = 'round'; tctx.lineJoin = 'round'; tctx.strokeStyle = baseColor; tctx.fillStyle = baseColor;
// Darkness, 0.5x at no pressure up to fully opaque at the neutral 0.5. This clamp,
// unlike the width's, is real and stays: alpha is already 1 at neutral, so there is
// no headroom to press into. Lowering the neutral to make room would re-render every
// pencil stroke ever drawn - lighter - so above neutral the pencil says pressure in
// its width alone.
smoothStroke(tctx, pp, pw, (i) => { tctx.globalAlpha = 0.5 + 0.5 * Math.min(1, prAt(pp, i) * 2); });
tctx.globalAlpha = 1; tctx.globalCompositeOperation = 'destination-in';
const pat = tctx.createPattern(grainTile(), 'repeat');
Expand Down
29 changes: 29 additions & 0 deletions test/canvas/pixels.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,35 @@ test('an eraser stroke takes ink away where it crosses', () => {
assert.ok(after > 0, 'the eraser took the whole stroke, not a band of it');
});

/** A straight pencil stroke at one pressure, and how thick it came out. */
function pencilThickness(pressure, pen = true, size = 24) {
const { c, ctx } = fresh();
const points = [];
for (let x = 20; x <= 180; x += 10) points.push({ x, y: 60, pressure });
drawStrokesOnCtx(ctx, [stroke({ tool: 'pencil', size, pen, points })], false, new Map());
const { box } = ink(c);
return box ? box.y1 - box.y0 + 1 : 0;
}

test('a pencil keeps widening above the neutral pressure', () => {
// The width term was wrapped in `Math.min(1, pr * 2)`, which stopped growing at 0.5 - so
// every press above neutral drew the same stroke to the pixel, and a stylus (which writes
// well above 0.5) behaved as though pressure were switched off. That is #338's "pencil
// pressure is ignored"; on main these three numbers are all equal.
const a = pencilThickness(0.5), b = pencilThickness(0.75), d = pencilThickness(1);
assert.ok(b > a, `0.75 is no thicker than neutral: ${a} -> ${b}`);
assert.ok(d > b, `a full press is no thicker than 0.75: ${b} -> ${d}`);
});

test('a pencil drawn with no pressure is drawn at the neutral weight', () => {
// The invariant that keeps the curve change off existing artwork: a mouse reports no
// pressure, which reads as 0.5, and the width at 0.5 is exactly 1x. So any future reshaping
// of the pencil's pressure response has to keep passing through 1 at neutral, or every
// pencil stroke ever drawn re-renders differently.
const mouse = pencilThickness(0.5, false), neutralPen = pencilThickness(0.5, true);
assert.equal(mouse, neutralPen);
});

/**
* The width an eraser actually clears, measured as the gap it cuts through a solid band.
* `pen` is set, so the pressure is trusted as a tablet's would be.
Expand Down
Loading