Foundations · 15

The Line of Sight

Two materials in this system already answer to where the reader is — the foil turns its rake as type travels up the screen, and the field slides its pool of light down a section as the reader passes it. Both were built on a line-of-sight model, and both read only one of that model's two axes. This chapter is the other axis, the rule that governs what may be allowed to move with it, and the one file in the system that measures a reader rather than an element.

The model, and the half of it that was missing

Holographic stock is not a picture of a spectrum. It is a surface that hands you a different part of one from every angle, which is why a foil catalogue is photographed tilting and a printed one looks dead. The system's answer, stated in Colour and built in base.css, is that you see the material as drawn when you are square to it — 116.57° is the head-on rake, and the rake flattens towards 90° as the line of sight goes oblique.

A view() timeline supplies that obliquity for free on one axis: type at the middle of the viewport is level with the reader's eye, type at either edge is not. But a view timeline measures the element, and the model is about the reader. Vertically the two coincide, because an eye at the middle of the screen is the only assumption available to a stylesheet. Horizontally they come apart the moment a viewport is wider than a headline: on a 1920 frame, a title at the vertical centre and hard against the left margin was being drawn perfectly head-on while the reader sat a foot to its right.

26.57° 90° 26.57° the material rake 90° rake 116.57° rake 90°
Plan view — the reader's position sets the rake
The angles in the diagram are the answer, not an illustration of it. A reader square to the material sees it at 90° to the screen plane and gets the designed 116.57° rake. A reader far enough to the side that the sight line has fallen to 26.57° — the isometric angle — gets the flat 90° rake. The system's own geometry supplies both ends, which is why this needed no new angle and no new colour.

Live

Move the pointer left and right across the panel. The type is not moving and no colour is changing: the angle the light crosses the letterforms at opens as you come square to them and flattens as you go oblique. Scroll instead of moving sideways and the other axis does the same thing. The readout is the two numbers the foil is currently being drawn with.

Jetzt Projekt starten!

‑‑sight‑v
1.000
‑‑sight‑h
1.000
rake
116.57°
state
resting
If the readout says resting and stays there, this browser or this reader is on one of the fallback paths — a coarse pointer, reduced motion, or no support for scroll-driven animations. That is not a failure state. It is the designed still, and it is what the two shipping pages render for everyone the script declines to run for.

The field, on the same source

The pool of light on the ground takes the same measurement as a position rather than as an angle. Vertically it is already scrubbed off the section's view() timeline; horizontally it now stands where the reader does.

the floor lights where you stand

The composition

Two squarenesses, each a number in [0, 1], and they multiply.

--foil-angle: calc(var(--angle-square) + var(--angle-b) * var(--sight-v) * var(--sight-h));

Summing two swings could leave the band; a product of two numbers in [0, 1] cannot, so the rake stays inside 90°–116.57° by construction. Every contrast figure the foil has ever been measured at therefore holds unchanged and unre-measured — turning a linear gradient changes where each stop lands, never which stops are present, and the whole ramp is still inside the clip box at every frame.

The product is also the physically true composition. A foil is head-on only when you are square to it on both axes; being level with a sheet you happen to be standing beside does not let you see it face on. Two independent swings added together would have said the opposite — that obliquity on one axis can be paid off by squareness on the other.

The swing is now derived rather than asserted. The comment in base.css has always said the 26.57° between the endpoints is the isometric angle itself. The keyframes said 90deg and 116.57deg and left that a coincidence a later edit could quietly break. It is now written as --angle-square + --angle-b: move the isometric angle and the foil follows it.

What may answer to sight

This is the part that keeps the idea from becoming a toy. A pointer-reactive surface is the easiest thing in the system to overspend, and the constraint is not taste — it is that the reader's position must never be able to change how legible anything is.

May move with sightWhy it is safe
The angle a gradient is drawn atTurning a ramp moves its stops; it adds and removes none of them. The worst sample is unchanged.
The position of a mask or a lightA mask multiplies by an alpha at or below 1, so every frame is at or under the unmasked floor already measured.
May notWhy not
Colour, ink strength, opacity of textContrast becomes a function of where a mouse is. There is no frame to measure, and a keyboard reader has no way to reach the legible one.
A composited specular or bloom over typeAlready rejected for the vertical axis, for the same reason: it lifts contrast on the lit half and drops it on the ink half.
Position, size or layout of anythingContent that moves away from the pointer cannot be clicked, and content that moves towards it is a trap. Sight moves light, never geometry.
Anything a reader must perceive to use the pageHalf of all readers have no fine pointer at all. Sight is the last layer, never the carrier.
One source, one page. There is a single reader and therefore a single light. Two materials on one screen answering to the pointer with different falloffs, or in opposite directions, is two light sources in one room — which is the thing the whole illustration chapter forbids at object scale.

Anatomy

PropertyWritten byRangeRestMoves
--sight-vview() keyframes0 → 1 → 01the foil's rake, vertically
--sight-hcf-sight.js0 … 11the foil's rake, horizontally
--field-light-yview() keyframes−20% … 120%50%the pool, down the section
--field-light-xcf-sight.js0% … 100%50%the pool, across the section

--field-light-x is the only one of the four declared inherits: true, and the asymmetry is forced rather than chosen: the pool is painted by .cf-ground::before, a script cannot set a property on a pseudo-element, so the only way in is to declare it on the originating element and let the pseudo inherit it.

Markup

None. Nothing opts in with a class or an attribute — the script finds the two materials that already exist. All a page does is load the file, last, after the stylesheets:

<script src="/design-system/assets/js/cf-sight.js" defer></script>
It is optional in the sense cf-stream.js established. Delete the tag and every page renders exactly as it did before the file existed — not approximately, but to the digit, because --sight-h and --field-light-x rest at the identity values of the expressions that consume them.

Every path

ConditionVerticalHorizontalWhat is rendered
Fine pointer, motion allowedrunsrunsboth axes
Coarse pointer (phone, tablet)runsreststhe vertical swing, as before
Pointer has left the windowrunseases back to restthe vertical swing, as before
prefers-reduced-motionrestsreststhe designer's 116.57° still
No scroll-driven animationsrestsreststhe designer's 116.57° still
Script blocked, deleted or failedrunsreststhe vertical swing, as before
No @propertyrestsrestssolid type, no clip — the pre-foil fallback
Printneithersolid Schwarz type; the field is not printed
Forced coloursneitherCanvasText; the field is removed

The script gates itself on CSS.supports('animation-timeline', 'view()') — the same test base.css puts on the swing — so the stylesheet and the file cannot disagree about whether the vertical half is running. It re-checks on every media-query change and puts both materials back the moment a condition stops holding.

What it costs

Rules

Three ways to get it wrong

  1. Making it bigger so it reads. The correct amount of this is the amount that is only noticed on the second look. If it cannot be seen at 26.57° of rake, the answer is that the reader is looking at the type rather than at the effect, which is the right outcome.
  2. Adding a tilt. A card that leans towards the pointer is the default in every holographic demo on the web, and it is skeuomorphism: this brand does not draw objects in perspective, it constructs them in 2:1 isometry, and an element that rotates has left the lattice.
  3. Reading a touch as a position. A finger has no position between taps. Driving light from touch coordinates makes the page flinch every time it is scrolled, which is why (pointer: fine) is a gate and not a preference.