Skip to content

Releases: ppardi/notes

6.99.44 — drop shading, a full arrangement, and deep links into the tree

Choose a tag to compare

@ppardi ppardi released this 21 Oct 22:52

Follow-ups to the category arrangement in 6.99.43.

Dragging

The shading says how far a drop reaches. Aiming at the middle of a row
shaded the row and everything under it, including the part of the tree a
collapsed category hides — more than the drop would touch. It now shades only
what the drop will actually reach.

A full arrangement says so. There is a limit on how many categories the
order can remember. Past it, a drag used to look like it had simply done
nothing: the row went back where it came from with no explanation. It now says
the arrangement is full.

Opening a link to a note

A link to a note filed deep in the tree opens the note. Following one
landed on the note, but the tree stayed as it was, so the note being read sat
inside categories that looked closed. The tree now opens far enough to show it
— only as far as that, and the collapse you made elsewhere is left alone and
not written over.

6.99.43 — Categories stay where you put them

Choose a tag to compare

@ppardi ppardi released this 21 Oct 20:53

Categories stay in the order you put them in.

Until now the sidebar sorted them by name and there was no way to change it.
That is also what made dragging one dishonest: a drop between two rows drew
the horizontal line every list uses to mean "it lands here", and then the tree
re-sorted and put the category somewhere else entirely. Giving categories a
real order is what lets that line tell the truth, so the two are one change.

Arranging them

Drag a category to a slot and it lands in that slot. Nothing else moves. A
category with children travels as a unit — the parent lands where you dropped
it and its children keep their order among themselves.

The arrangement is stored per user on the server, so it follows you between
devices and survives a reload. A category you make appears at the top of its
group, where you will see it. One the arrangement has no place for yet — a
folder you made in the Files app, or anything from before this release —
appears at the top of its group too, sorted by name among the others, until
you drag it somewhere.

Smart categories are arranged alongside the real ones and can go anywhere
among them.

There are no sorting controls. The order is whatever you arranged.

Saying where a drop will land

Each row has three bands rather than the old quarter-at-each-edge split, which
left about eleven pixels to aim at and was why the line flickered in and out.

  • The middle nests: the row and its children shade, because what you are
    aiming at is the group, not the row.
  • The top and bottom place it beside, at that row's own level: a line, in
    the slot it will take.

Never both at once. Smart categories take a drop beside them now — they
refused every drop before, which left the slots above and below them
unreachable. They still refuse notes.

Two things that were quietly wrong

A category you emptied was destroyed. Moving or deleting its last note
took the category with it, and every empty parent above it. That made sense
when a category was only ever a side effect of where a note sat; it is wrong
when you made the category on purpose. Nothing removes a category now except
being asked to.

A category nested inside another was never reported to the browser at all.
Clients/Client1 could not be filed into, chosen, or brought back by a
reload, while Clients could — the listing merged each folder's categories
with its subfolders' in a way that silently dropped the deeper ones. It only
ever bit a category with no notes directly inside it, which is exactly what
this release makes possible.

Elsewhere

The note menu no longer invents categories. Its category field took
anything typed into it and filed the note under whatever that was, creating
the folder on the way. A path typed with a slip in it — Clients/Client1/Porjects
— made exactly that and said nothing. It offers the categories that exist now.
Typing still narrows the list, which is how you reach a deep one. The REST API
is unchanged: filing a note into a category that does not exist still creates
it, which is how the mobile clients work.

The app opens where you left it, and on all your notes when there is
nothing to remember — rather than guessing a category and hiding the rest.

A collapsed category stays collapsed. Opening the app on a note filed deep
in the tree used to open every category above it and remember that, undoing a
collapse you had made.

Worth knowing

Nothing about the file format changes and nothing already written is altered.
If you have never arranged anything, the sidebar looks exactly as it did.

6.99.42 — Categories become things you make

Choose a tag to compare

@ppardi ppardi released this 21 Oct 02:17

Categories become things you make, rather than things that happen to you.

A category in this fork is a folder you create deliberately, from the sidebar.
Almost everything else about them still assumed the upstream model, where a
category existed only as a side effect of where a note happened to sit. This
release closes that gap. Five fixes, and the first one is the one that started
it.

A new category was real in one browser tab and nowhere else

Making a category from the sidebar put it in the browser and sent nothing to
the server. There was no request to send, and no route to send one to. So the
category existed in that tab and nowhere else: dragging it somewhere answered
"Note not found", renaming it from its menu failed the same way, and a
reload lost it altogether.

The rename it sends looks its folder up without permission to create one,
finds nothing, and the missing folder is reported as a missing note — which is
where that strange message came from.

Making one now creates the folder, with any parents it needs, before anything
appears in the sidebar. A category you can see is one that exists. It is
stored under the name the server could actually use, too: a folder name cannot
hold a colon or a backslash, and showing one that the folder does not have
would be the same lie in a smaller way.

A category you emptied was destroyed

Moving or deleting the last note in a category took the category with it, and
every empty parent above it.

That behaviour makes sense upstream, where there is no way to make a category:
filing a note as Projects/2024/Q1 is the only thing that brings those three
folders into being, so moving that note away leaves three nobody asked for.
Here it is wrong. Someone who builds the structure before filling it has
categories that are empty for a while, and one that was filled and later
emptied is a shelf they built, not debris.

Nothing removes a category now except being asked to.

Categories nested inside another were never reported at all

The listing gathered each folder's categories and then merged that list with
the one from each subfolder — using an operation that, on lists numbered from
zero, keeps the entry it already has wherever the numbering collides. So the
first category at one level hid the first one below it, the second hid the
second, and so on. A nested category only survived if its parent's level
happened to be longer than its own.

Clients/Client1 never reached the browser while Clients did. A category
with no notes directly inside it could not be filed into, chosen, or brought
back by a reload: nothing had reported that it existed.

This went unnoticed for a long time because a category holding a note is also
reconstructed from that note's own path, which is the ordinary case. It only
bites a category that holds no notes — which is exactly what this release
makes possible.

The note menu invented categories from typos

The category field in a note's actions menu accepted anything typed into it
and filed the note under whatever that was, creating the folder on the way.
Type Clients/Client1/Porjects with a slip in it and that is what you got:
the folder made, the note moved into it, and nothing said so. The note simply
left the list it was in and arrived somewhere that looked right at a glance.

The field now offers the categories that exist and nothing else. Typing still
narrows the list, which is how a deep path is reached quickly — that is the
field's own filtering and it is unaffected.

The REST API is unchanged. Filing a note into a category that does not
exist still creates it, which is the documented contract the Android and iOS
clients rely on.

An empty category showed you the last note you had open

Opening a category with nothing in it left whatever note was open before
sitting in the pane beside it — a note from somewhere else entirely, with
nothing to say the category being looked at was empty. And with nothing open
at all, the first-run welcome screen replaced the list, the search field and
the New note button.

The list stays now, whatever the pane beside it holds: the note, the welcome
on a genuine first run, or a line saying why it is empty — No notes in this
category
, No notes found, Select a note.

Worth knowing

Nothing about the file format changes, and nothing already written is altered.
A category that exists only because a note sits in it behaves exactly as
before.

If you have empty folders in your Notes directory that you did not expect,
they were always there — they simply could not be reported until now.

6.99.41 — Scribble stays out

Pre-release

Choose a tag to compare

@ppardi ppardi released this 06 Oct 17:56

Scribble stays out, even after the pen has been somewhere else.

What was wrong

Move the pen into the browser's own address bar, then back down onto the
canvas, and from that moment the handwriting stopped being ink: iPadOS
recognised it and put the text into the note underneath.

The dialog's whole defence against Scribble is holding the focus. Nothing
inside it is a text field, so while the focus is in there a recogniser has
nothing to write into. That focus was claimed once, when the dialog opened,
and again only if the dialog was resized — so it lasted exactly until
something took it away.

The address bar is browser chrome. It is outside the page altogether, so
nothing fires on the document when the pen goes there, and on the way back
WebKit hands the focus wherever it likes. That leaves the note's editor as the
nearest text field, sitting live behind the canvas.

The fix

Two halves, because holding the focus is a race this dialog can lose.

  • The focus is taken back whenever it lands outside — on focusin, and on
    the window getting the focus back. The window is the one that matters here,
    since the document hears nothing while the pen is in the browser's chrome.
    A colour or tool menu that is open is still left alone: each is a popover,
    teleported out of the dialog, and taking the focus back would shut it
    mid-choice.

  • What the dialog covers is made inert while it is open — which removes
    what a recogniser would write into rather than racing it for the focus. The
    editor is handed back before the focus is, because focusing an inert element
    does nothing at all and the note would be left without a caret. An inert
    that was already there is left alone, and the element put out of reach is
    remembered rather than looked up again, since the editor is replaced when a
    note is reloaded.

Worth knowing

If Scribble has already engaged before the pen comes back, there is now
nothing behind the canvas to write into — but iPadOS may still withhold the
pen from the canvas for that stroke. Ink that simply does not register, rather
than turning into text in the note, is that case: the second half working and
the first not being enough.

Nothing about the file format changes and nothing already drawn is altered.

6.99.40 — the pen's samples are taken once

Choose a tag to compare

@ppardi ppardi released this 06 Oct 17:16

The pen's samples are taken once.

What was wrong

Handwriting came out rough on a real server while the same iPad, the same
build and the same browser drew cleanly on an almost empty page. The
difference was in the captured data, not in the drawing.

getCoalescedEvents() reports the positions the digitizer gathered since the
last animation frame, not since the last move. Where one pointermove is
dispatched per frame the two are the same thing and the batches never overlap.
Where more than one is dispatched in a frame, each is handed the same batch —
and taking every batch whole put the stroke through the same run of samples
twice. The pen ran forward, jumped back and retraced itself, over and over.

The earlier filter could not see it. That one drops a sample identical to the
one immediately before it, which is the right shape for the Pencil reporting a
position twice in a row. Here a whole run of different positions arrives
twice: nothing in A B C D A B C D is an adjacent duplicate, so the entire
second copy went in.

Measured, in a drawing made on a working server

  • 48.5% of 1709 samples were repeats of a point the stroke had already
    visited.
  • 164 runs of four samples appeared twice over.
  • One stroke jumped backwards 64 times, as far as 28.9 px.

The same iPad on a near-empty page: 0.5–2.0%, and no repeated runs at all.
Which is why this survived every test on the development harness and appeared
the first time the app met a real server and a real note.

The fix

A sample's own time is what tells the batches apart — positions repeat, a pen
held still reports the same place for as long as it is there, but the time a
reading was taken does not. The canvas remembers the newest time it has taken
and ignores anything not newer, so a batch handed over a second time costs
nothing.

It is applied only to a coalesced batch. A move carrying a single position is
left alone: a repeat of that is a repeat of a place, which the earlier filter
already drops, and the move's own timestamp is no use here because Safari
rounds it for privacy and two moves inside one millisecond share it.

Nothing about the file format changes, and nothing already drawn is altered —
the roughness in a drawing saved before this version is in its stored samples
and stays there.

6.99.39 — ink draws shapes

Pre-release

Choose a tag to compare

@ppardi ppardi released this 02 Oct 20:13

Ink draws shapes.

Three shapes, and the pen among them

A tool button in the toolbar showing what the next stroke will be, opening four
choices: Pen, Line, Rectangle, Ellipse. Drag to place one — the drag sets two
opposite corners, and a rectangle spans them while an ellipse is inscribed in
them.

The pen is a member of the set rather than the absence of one, so the mode has a
single home and what you are about to draw is where your eye already is. Unlike
the color, the tool is not remembered between drawings: a color is a
preference, a tool is a momentary intent, and someone coming back to a note
means to write.

A line within 5° of flat or upright snaps true, which is what makes a
section delimiter worth having. Rectangles and ellipses do not snap — a square
is a rectangle dragged square by eye, which is the cost of never fighting
someone who meant the proportions they drew.

A shape is a stroke

A shape is stored as one densely sampled polyline, exactly like handwriting.
That is the whole design, and it is why nothing else had to change: the eraser
rubs out a rectangle's edge as it rubs out a word, Undo takes back the shape in
one press, the shape takes the ink color in force, and cropping, the picture,
the saved file and the theme filter are untouched.

A drag whose diagonal stays under 8 px commits nothing, so a stray tap leaves no
dot. Choosing a tool puts the eraser away, the rule choosing a color already
followed. A finger still pans and still never draws.

Drawing a shape is not drawing with a nib

This is the one place a shape is not treated exactly like handwriting, and it
was conceded to the device rather than chosen.

perfect-freehand exists to turn a hand's samples into a nib's mark: it smooths
the jitter and wraps the path in an outline. A construction has no jitter, so
the smoothing had nothing to do but cut the corners — a 200×120 rectangle's
corner landed 1.75 px inside itself where a round join reaches 1.77 px
outside, and the edges were seen to taper into every turn. Worse, that outline
runs up one side of the path and back down the other, so a closed shape's
outline doubles back on itself where it began: WebKit fills that overlap half
a nib wider than the band
, putting a blister on the line exactly where every
rectangle and ellipse starts. Chromium fills it flat, which is why this took an
iPad to find.

A shape now carries ruled: true and is drawn as the path it is — the nib's
width, a round join and cap, and a closed path for a loop. Measured along the
top edge through the join, ink per column is flat to 0.00 in Chromium and 0.01
in WebKit, and the corner reaches 1.75 px past itself in both.

What a file written by this version means elsewhere

ruled is additive and its absence means "a hand", so every drawing saved
before this version is unaffected and every freehand stroke still is. A shape
opened by an older build is still the same polyline and still draws — it just
draws with the smoothing, which is what that build did anyway.

Also

  • Opening the color picker shuts the tool menu, and the other way round. Two
    popovers over one small bar, and only one decision at a time.
  • Panning the page mid-drag no longer stretches the shape being dragged, and
    Undo during a drag takes back the shape rather than a stroke you meant to
    keep.

6.99.38 — ink in six colors

Pre-release

Choose a tag to compare

@ppardi ppardi released this 02 Oct 01:55

Ink has six colors.

Picking a color

One button in the toolbar, filled with the color in use, opening the six:
Ink, Red, Orange, Green, Blue, Purple. The color applies to the next stroke
and never to what is already on the page, so changing it is not an edit. The
choice is remembered between drawings.

Each color carries its name rather than being a colored square alone — a color
is no label at all to a reader who cannot tell two of them apart, or who is
listening. The default is called Ink and not Black, because it is the one
the theme is applied to: on a dark theme it draws white.

The colors were measured, not chosen by eye

Every one clears 4.5:1 against the light theme's background and against both
dark ones, after the theme has been applied. The weakest are green at 4.6:1 in
light and orange at 7.0:1 in dark. A seventh color needs the same two numbers
before it belongs in the set.

Keeping the hue on a dark theme

Ink is stored as one color on transparency and the theme is applied where the
ink is shown — that is what lets a page written on a light screen read on a
dark one, and why drawings saved before any of this still follow the theme.

The filter was plain invert, which flips hue along with lightness: a red
annotation came out cyan. It is now invert(100%) hue-rotate(180deg), which
flips the lightness and puts the hue back — what PencilKit does. A dark red
becomes a light red. Black still becomes white, so every drawing saved before
this version is unaffected
.

Measured in two engines rather than one: the six colors come back byte-identical
to the filter specification's own matrix in Blink, and within 2/255 of that in
WebKit, which is what the iPad runs.

A pen tap now reaches the tools

Tapping Erase, Undo, Cancel, Done or the color button with a Pencil did nothing;
a finger worked. The dialog refuses touchmove so that a stroke dragged over the
toolbar is not handed to Scribble as handwriting, and refusing it is enough for
WebKit to call a tap a drag and synthesize no click at all. A finger stays inside
the movement the browser ignores; a Pencil does not.

The tools are served from pointerup as well now, for a pen only. A click
arriving within a moment of one is the same tap twice and is dropped, so a
finger, a mouse and the keyboard are unaffected.

Choosing a color puts the eraser away

Picking a color says the next thing is a stroke. Leaving erase mode on was worse
than inconvenient — the reader meant to write, and what they got instead took
away strokes they meant to keep. The menu also closes on the choice, including
when the color chosen is the one already in use.

What a file written by this version means elsewhere

A stroke carries the color it was drawn in, and every stroke is given a color the
palette offers as it enters the canvas — so the picture and the strokes stored
beside it can never disagree. A drawing opened by an older build draws its
strokes black while showing the colored picture; nothing is lost, but saving it
there would flatten it.

Also

  • The color swatches line up with their names. A 16px dot in a slot built for a
    34px icon sat 8.8px high and hard against its label.
  • dev/ipad.sh --dark and --light switch the development container's theme.
    The e2e harness pins enforce_theme to light for deterministic screenshots,
    which silently overrode every per-account choice.

6.99.37 — ink the size it was drawn, and smoother

Choose a tag to compare

@ppardi ppardi released this 01 Oct 19:51

Everything reported against v6.99.35 and v6.99.36, measured on the device
rather than guessed at.

A drawing is now the size it was drawn, and sharp

Cropping the picture to the writing in v6.99.35 removed an accident along with
the white space: the old whole-screen picture was being shrunk to fit and
downsampled on the way. Measured at 2x, a 420 pixel picture was laid out at 420
CSS pixels — a 2.00x upscale, and 2.10x the size it was drawn.

Markdown cannot say how big to show an image, so the fix is a pair: ink is saved
at two image pixels for every CSS pixel of drawing, and shown at half size. That
also removed a bug nobody had reported — the same handwriting came out twice as
large in the note if it had been written on the iPad rather than the Mac.

Ink saved by v6.99.34 to v6.99.36 shows at half its size until it is opened and
saved again.

The ink itself is smoother

Two separate faults, both only visible since the nib was thinned to 2.5 CSS
pixels. A wide nib covers them; a fine one draws them.

The Pencil reports every position twice. In a page of handwriting on the
device: of 1038 gaps between consecutive samples, 513 were exactly nought. A
repeated reading says nothing the first one did not, and the pair is a flat step,
so the curve the smoothing followed was a staircase. Dropped rather than
averaged — there is nothing to average. It also halves what a page stores.

And writing fast it leaves gaps. Up to 17 CSS pixels between one reading and
the next, and two long segments meeting is a corner. The path is now filled in
from a Catmull-Rom curve through the readings — a straight fill would put more
points on the same corner and change nothing. Every reading is kept and passed
through, so letter shapes do not move and a ruled line stays straight. Only
steps above 6 pixels are touched; the median is 4.3.

More page than one screen

The canvas is three screens tall, moved with a finger — a finger never draws, so
there is no mode to switch. Gradients at the top and bottom edges show there is
more page in that direction. It stays one tall image.

A drawing reopens where it was left

A saved block is cropped to the writing, so without its position it jumped to the
top left: you saw it where you wrote it the first time and somewhere else every
time after. The position now travels in the file. Where the screen cannot hold it
at that offset — an iPad is a different width in portrait — the ink is brought on
rather than left hanging off an edge where it can be neither seen nor rubbed out.

Ink saved before v6.99.34 carries no position and still opens in the corner.

The tools are icons, and the Ink button stays put

Erase, undo and cancel are labelled icon buttons; Done keeps its word, because it
is the one that ends the drawing.

The Ink button rode up the page as a note grew. Its wrapper was one screen tall
inside a note many screens long, and a sticky element only sticks while its own
box is on screen. The wrapper is now as tall as the note.

6.99.36 — the stale reopen, and an eraser that takes the first pass

Choose a tag to compare

@ppardi ppardi released this 01 Oct 16:37

Two of the three things reported against v6.99.35.

Tapping the ink opened the previous drawing

The attachment endpoint answers private, max-age=3600 and sends no ETag, so
the browser serves a second read of the same path out of its cache for an hour
without asking the server. An ink file changes under a fixed name every time it
is saved, so that cached answer is the drawing as it stood before the last
edit — and the canvas opened on it. v6.99.34 fixed this for the picture shown in
the note; the file the canvas itself reads was still being cached.

Reproduced in a desktop browser before fixing: the second reopen loaded exactly
the pixel count the first one had.

The eraser needed several passes

It measured each sample the browser reported, not the path between them. The pen
is reported tens of pixels at a time, so a thin line crossed between two reports
was a line you watched the eraser go through without taking.

It now tests the eraser's path against the stroke, segment against segment, and
tests crossing before measuring any distance — two segments that cross are
nought apart, and no measurement from an end point will say so. The path is
carried across reports too, so the gap between one and the next is covered;
never across a lift, since the line back to where the eraser was last put down
would take whatever it crossed.

The eraser now draws its own outline under the pen, so it can be aimed. An
outline rather than a disc, because what matters is seeing which marks are
inside it.

Still open

The canvas opening at the size of the previous save. Not reproduced: in a
desktop browser the dialog is the full viewport both on a first open and on a
reopen over a small picture. Needs a look at the device.

6.99.35 — an eraser, cropped ink, and the iPad bugs

Choose a tag to compare

@ppardi ppardi released this 01 Oct 15:53

An eraser, and the four ink bugs from the iPad. (v6.99.34 was built but never
published; everything in it is here.)

The eraser

A toggle in the bar, beside Undo. It rubs out whole strokes — the ones it
is dragged across. A long mark therefore goes all at once: crossing an
underline takes the whole line. Splitting strokes where the eraser meets them
is the upgrade if that turns out too coarse in the hand.

It reaches as far as the ink does rather than to the samples down the middle of
a stroke, and it is a good deal wider than the nib — a target two pixels across
aimed at with a pen in your hand would never be hit. A finger never erases, for
the same reason it never draws.

Undo now reverses the last thing you did, drawn or erased, rather than
popping the last stroke. One pass of the eraser is one thing to undo however
much it crossed, and the strokes go back where they came from. A page you have
only opened has nothing to undo: the ink already in the file is not something
this canvas did, and the eraser is how an old mark comes out.

The four bugs

The picture is cropped to the writing. It was the size of the device, so a
note full of ink was mostly white space. The strokes move with the crop, so
reopening draws them from the picture's own corner — which also means ink
written on a wide screen still fits on a narrow one.

The canvas takes the pen as soon as it opens. Nothing was drawn until the
existing ink had been fetched, so a stroke begun in that window was dropped in
silence: "I have to tap twice before the nib registers." Ink that arrives is
put underneath whatever was drawn meanwhile. Saving still waits for it.

Writing over the buttons no longer becomes Scribble text in the note. The
whole dialog refuses a touch that moves, where only the sheet did before, and
the dialog takes the focus — the recogniser writes into the focused text field,
and the editor underneath still had it.

The note shows the ink that was just saved. Re-editing writes the file under
the name the note already holds, so nothing in the document changed and the
browser answered out of its cache. Leaving and re-entering the note is no
longer needed.

Also: a save that failed leaves a page that can still be written on, which is
what its message promises.