Arranging: boxes on everything, handwriting movable, the Eraser's preview, a Circle shape, pixel-exact cues

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Fable 5.1 committed 2026-10-14 20:38:29 +02:00
1 parent 03907da8c9
commit 24a6a2595b
10 files changed
+690 -126

No files matched your search

+27 -5
View File
@@ -31,11 +31,11 @@ import { NOMINAL_PRESSURE } from './pen_input.js';
import { POINT_SIZE } from './page_model.js';
/** The shapes, in the order of the menu. The first is the default. */
export const SHAPES = /** @type {const} */ (['rectangle', 'ellipse', 'line', 'arrow']);
export const SHAPES = /** @type {const} */ (['rectangle', 'circle', 'ellipse', 'line', 'arrow']);
/** @typedef {typeof SHAPES[number]} Shape */
/** @type {Record<Shape, string>} What the button and the menu call each shape. */
export const SHAPE_NAMES = { rectangle: 'Rectangle', ellipse: 'Ellipse', line: 'Line', arrow: 'Arrow' };
export const SHAPE_NAMES = { rectangle: 'Rectangle', circle: 'Circle', ellipse: 'Ellipse', line: 'Line', arrow: 'Arrow' };
/** The key under which the browser remembers the chosen shape. */
export const SHAPE_KEY = 'kokkoja_shape';
@@ -149,7 +149,10 @@ export function ellipsePoints(x0, y0, x1, y1) {
// Ramanujan's approximation of the circumference.
const h = ((rx - ry) / (rx + ry)) ** 2 || 0;
const circumference = Math.PI * (rx + ry) * (1 + 3 * h / (10 + Math.sqrt(4 - 3 * h)));
const count = Math.max(LEAST_ELLIPSE_POINTS, Math.min(MOST_ELLIPSE_POINTS, Math.ceil(circumference / ELLIPSE_STEP)));
// A multiple of four, so that a point lies at each end of both axes and
// the rectangle around the points is exactly the ellipse's (Arrange's
// box and a circle's proportions depend on it).
const count = Math.max(LEAST_ELLIPSE_POINTS, Math.min(MOST_ELLIPSE_POINTS, 4 * Math.ceil(circumference / ELLIPSE_STEP / 4)));
/** @type {number[]} */
const points = [];
for (let i = 0; i <= count; i++) {
@@ -159,6 +162,21 @@ export function ellipsePoints(x0, y0, x1, y1) {
return points;
}
/**
* A circle about its centre (cx, cy), the radius reaching the dragged
* point (Andy, 2026-10-14: the centre first, then the radius in any
* direction).
* @param {number} cx
* @param {number} cy
* @param {number} x
* @param {number} y
* @returns {number[]}
*/
export function circlePoints(cx, cy, x, y) {
const radius = Math.hypot(x - cx, y - cy);
return ellipsePoints(cx - radius, cy - radius, cx + radius, cy + radius);
}
/**
* An arrow from (x0, y0) to its tip at (x1, y1), as the outline of a
* filled polygon: the shaft as wide as the line, the head wider, its
@@ -202,7 +220,9 @@ export function arrowPoints(x0, y0, x1, y1, width) {
}
/**
* The points of a shape dragged open from (x0, y0) to (x1, y1).
* The points of a shape dragged open from (x0, y0) to (x1, y1): a
* circle from its centre to a point of its edge, the others from one
* corner of their rectangle to the opposite one.
* @param {Shape} shape Not 'line', which is made of taps.
* @param {number} x0
* @param {number} y0
@@ -214,6 +234,7 @@ export function arrowPoints(x0, y0, x1, y1, width) {
export function draggedPoints(shape, x0, y0, x1, y1, width) {
switch (shape) {
case 'rectangle': return rectanglePoints(x0, y0, x1, y1);
case 'circle': return circlePoints(x0, y0, x1, y1);
case 'ellipse': return ellipsePoints(x0, y0, x1, y1);
case 'arrow': return arrowPoints(x0, y0, x1, y1, width);
default: return straightPoints([[x0, y0], [x1, y1]]);
@@ -224,7 +245,8 @@ export function draggedPoints(shape, x0, y0, x1, y1, width) {
* The points of a shape moved or resized by Arrange from the rectangle
* around its points to another: every point carried along (a rectangle
* stays a rectangle, an ellipse an ellipse, a line keeps its angles'
* proportions), except an arrow, which is made afresh from its tail and
* proportions; a circle is resized with its shape kept, so stays one),
* except an arrow, which is made afresh from its tail and
* tip carried along, so that its head and shaft keep the line's width.
* @param {import('./page_model.js').Stroke} stroke
* @param {import('./page_model.js').Rectangle} from