@use "sass:color" as sass-color; @use "@styles/color"; @use "@styles/var"; /* Everything on a page is a multiple of one ruled line. * * `--book-line` is the height of that line and the single source of truth for * the ruling, the text leading, the stamp positions and the caret. JS reads its * resolved pixel value back with getComputedStyle to paginate, and writes * `--book-lines` once it knows how many whole lines fit in the page body - so * the paper never ends on half a rule. */ .book { --book-line: #{var.$line-fallback}; --book-lines: 12; flex: 1 1 auto; display: flex; //Stretch, not centre: the pages have to inherit a definite height, since //how many ruled lines fit on one is measured from it. align-items: stretch; justify-content: center; min-width: 0; min-height: 0; position: relative; outline: 0; } /* The closed shell: the covers and the block of pages the spread sits on top of */ .book__shell { position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0; max-width: var.$book-max-width; border-radius: var.$book-radius; background-color: color.$paper-edge; box-shadow: var.$shadow-page; //The page block: a few stacked cut edges peeking out below and to the sides &::before { content: ""; position: absolute; inset: -0.35rem -0.5rem -0.55rem; z-index: -1; border-radius: calc(var.$book-radius + 2px); background-image: linear-gradient( to bottom, color.$paper-deep 0, color.$paper-edge 22%, color.$paper-deep 40%, color.$paper-edge 62%, color.$paper-deep 100% ); box-shadow: 0 0.9rem 1.6rem color.$shadow-deep; } } .book__spread { flex: 1 1 auto; display: flex; align-items: stretch; min-height: 0; position: relative; border-radius: var.$book-radius; overflow: hidden; background-color: color.$paper; //Depth for the turning leaf. Long, because a book seen from a chair is //nearly flat on - a short perspective makes the page fan out like a card //trick rather than lift off the gutter. perspective: 2600px; perspective-origin: 50% 45%; } /* The gutter. Two paper surfaces meeting is the one place the book needs real * shading: without it the spread reads as a single flat sheet. */ .book__spine { position: absolute; top: 0; bottom: 0; left: 50%; width: var.$spine-width; transform: translateX(-50%); pointer-events: none; z-index: 3; background-image: linear-gradient( to right, rgba(120, 96, 62, 0) 0%, rgba(120, 96, 62, 0.1) 32%, rgba(96, 74, 46, 0.3) 47%, rgba(72, 54, 32, 0.42) 50%, rgba(96, 74, 46, 0.3) 53%, rgba(120, 96, 62, 0.1) 68%, rgba(120, 96, 62, 0) 100% ); } /* A single page */ .page { flex: 1 1 50%; min-width: 0; position: relative; display: flex; flex-direction: column; padding: var.$page-padding clamp(1rem, 2.2vw, 2.25rem); //Paper, lit from the outside edge and darkening into the gutter background-color: color.$paper; background-image: radial-gradient(120% 80% at 50% 0%, rgba(255, 255, 255, 0.5), transparent 55%), repeating-linear-gradient( 117deg, rgba(120, 96, 62, 0.012) 0 2px, rgba(255, 255, 255, 0.02) 2px 5px ); } .page--left { background-image: linear-gradient(to right, rgba(255, 255, 255, 0.45), transparent 30%), linear-gradient(to right, transparent 70%, rgba(140, 112, 74, 0.09) 100%), repeating-linear-gradient( 117deg, rgba(120, 96, 62, 0.012) 0 2px, rgba(255, 255, 255, 0.02) 2px 5px ); box-shadow: inset -1px 0 0 rgba(140, 112, 74, 0.12); } .page--right { background-image: linear-gradient(to left, rgba(255, 255, 255, 0.45), transparent 30%), linear-gradient(to left, transparent 70%, rgba(140, 112, 74, 0.09) 100%), repeating-linear-gradient( 117deg, rgba(120, 96, 62, 0.012) 0 2px, rgba(255, 255, 255, 0.02) 2px 5px ); } /* The page body: margin column, then the ruled text column */ .page__body { flex: 1 1 auto; display: flex; align-items: flex-start; gap: 0; min-height: 0; position: relative; } .page__margin { flex: 0 0 var.$page-margin-width; position: relative; height: calc(var(--book-lines) * var(--book-line)); padding-right: 0.7rem; //The red margin rule every school notebook has border-right: 1px solid color.$rule-margin; } /* Where an entry begins, the margin says when it was written */ .page__stamp { position: absolute; right: 0.7rem; width: calc(100% - 0.7rem); text-align: right; font-family: var.$font-hand; line-height: 1.05; color: color.$ink-soft; pointer-events: none; } .page__stamp-date { display: block; font-size: 1.05rem; font-weight: 600; white-space: nowrap; } .page__stamp-time { display: block; font-size: 0.95rem; color: color.$ink-faint; } /* The text column. Height is an exact number of rules, so the ruling always * ends flush with the bottom of the column. */ .page__column { flex: 1 1 auto; min-width: 0; position: relative; height: calc(var(--book-lines) * var(--book-line)); padding-left: 0.9rem; //The ruling. Each rule sits at the bottom of its line's band. background-image: repeating-linear-gradient( to bottom, transparent 0, transparent calc(var(--book-line) - 1px), color.$rule calc(var(--book-line) - 1px), color.$rule var(--book-line) ); } /* Shared text metrics. * * The rendered lines, the hidden measurer and the input all have to wrap * identically - the measurer is what decides where the breaks are, the lines * are what the reader sees, and the input is what the arrow keys move through. * Any drift between the three shows up as a caret in the wrong place, so the * three of them take their typography from exactly one place: here. */ @mixin page-text-metrics { font-family: var.$font-hand; font-size: calc(var(--book-line) * 0.82); line-height: var(--book-line); letter-spacing: 0.005em; word-spacing: 0.02em; font-variant-ligatures: none; tab-size: 4; } .page__lines { @include page-text-metrics; position: relative; z-index: 1; color: color.$ink; } /* One visual line, already broken by the paginator - so it must never re-wrap */ .page__line { display: block; height: var(--book-line); white-space: pre; overflow: hidden; } /* An entry that was closed reads as past writing: same hand, a touch lighter */ .page__line--closed { color: sass-color.mix(color.$ink, color.$paper, 88%); } /* The hidden element the paginator measures in. * * Book.vue owns it and positions it inline over the real text column, at that * column's measured width - so it wraps at exactly the width the finished lines * will have, while staying one stable element across page turns. */ .page__measure { @include page-text-metrics; position: absolute; top: 0; left: 0; visibility: hidden; pointer-events: none; white-space: pre-wrap; overflow-wrap: break-word; z-index: -1; } /* The writing surface. * * A textarea cannot flow from the left page onto the right one, so it is not * what you look at: it is only where the keystrokes, the selection and the * arrow keys live. Its text is transparent and the book draws the glyphs, the * caret and the selection itself. It is sized and styled exactly like a page * column, which means its own soft wrapping matches the paginator's - so Up and * Down still move by the visual lines the reader can actually see. */ .page__input { @include page-text-metrics; //Placed inline by Book.vue over whichever column the caret is on, so the //element itself survives every page turn and keeps its focus and selection. position: absolute; top: 0; left: 0; z-index: 2; margin: 0; padding: 0; border: 0; resize: none; overflow: hidden; background: transparent; color: transparent; caret-color: transparent; //Clicks are handled by the page, which maps them to a character offset and //moves the real caret there. pointer-events: none; &::selection { background: transparent; } &:focus { outline: 0; } } /* The caret the book draws for itself */ .page__caret { position: absolute; width: 2px; height: calc(var(--book-line) * 0.66); margin-top: calc(var(--book-line) * 0.16); background-color: color.$ribbon; z-index: 4; pointer-events: none; animation: caret-blink 1.1s steps(1, end) infinite; } @keyframes caret-blink { 0%, 45% { opacity: 1; } 50%, 95% { opacity: 0; } 100% { opacity: 1; } } /* Selection, drawn per visual line since the native one is invisible */ .page__selection { position: absolute; height: calc(var(--book-line) * 0.86); margin-top: calc(var(--book-line) * 0.07); background-color: rgba(180, 126, 65, 0.3); z-index: 0; pointer-events: none; border-radius: 2px; } /* Clicking anywhere on the writing page puts the caret there */ .page--writable { cursor: text; } /* Page furniture */ .page__foot { flex: 0 0 auto; display: flex; align-items: flex-end; justify-content: space-between; gap: var.$elem-spacing; padding-top: 0.6rem; font-size: 0.72rem; color: color.$ink-faint; min-height: 1.6rem; } .page__number { font-family: var.$font-hand; font-size: 1rem; color: color.$ink-faint; } .page--left .page__foot { flex-direction: row-reverse; } .page__hint { font-family: var.$font-hand; font-size: 1.05rem; color: color.$ink-faint; } /* The placeholder, shown on a page with nothing written on it yet */ .page__placeholder { @include page-text-metrics; position: absolute; left: 0; right: 0; color: color.$ink-ghost; white-space: pre; pointer-events: none; z-index: 1; } /* Turning. * * A real leaf, hinged on the gutter. Going forward it is the right-hand page * that lifts and swings left - the direction your hand actually moves - and it * carries the next left-hand page on its back, so what lands is what you then * read. Going back is the mirror. * * The spread underneath shows a mix while this runs (see visiblePages): the * side the leaf lifted from keeps its old page until the leaf covers it. */ .book__leaf { position: absolute; top: 0; bottom: 0; width: 50%; z-index: 7; pointer-events: none; transform-style: preserve-3d; will-change: transform; } .book__leaf--forward { left: 50%; transform-origin: left center; animation: leaf-forward var.$trans-slow cubic-bezier(0.4, 0.06, 0.3, 0.96) forwards; } .book__leaf--back { left: 0; transform-origin: right center; animation: leaf-back var.$trans-slow cubic-bezier(0.4, 0.06, 0.3, 0.96) forwards; } //Right page sweeping left across the gutter @keyframes leaf-forward { from { transform: rotateY(0deg); } to { transform: rotateY(-180deg); } } //Left page sweeping right across the gutter @keyframes leaf-back { from { transform: rotateY(0deg); } to { transform: rotateY(180deg); } } .book__leaf-face { position: absolute; inset: 0; overflow: hidden; backface-visibility: hidden; background-color: color.$paper; box-shadow: 0 0 2rem rgba(20, 14, 9, 0.35); //The page fills the leaf rather than half a spread .page { width: 100%; height: 100%; } } //The back of a leaf is only seen once it has swung past upright .book__leaf-face--back { transform: rotateY(180deg); } /* Paper catches the light as it lifts and loses it as it lands. Two passes of * the same shade, offset by half the turn, is what sells the leaf as solid. */ .book__leaf-shade { position: absolute; inset: 0; pointer-events: none; } .book__leaf-face--front .book__leaf-shade { background-image: linear-gradient(to left, rgba(20, 14, 9, 0.42), rgba(20, 14, 9, 0.04) 45%, transparent 75%); animation: leaf-shade-out var.$trans-slow ease-in forwards; } .book__leaf-face--back .book__leaf-shade { background-image: linear-gradient(to right, rgba(20, 14, 9, 0.46), rgba(20, 14, 9, 0.06) 45%, transparent 75%); animation: leaf-shade-in var.$trans-slow ease-out forwards; } @keyframes leaf-shade-out { from { opacity: 0; } to { opacity: 1; } } @keyframes leaf-shade-in { from { opacity: 1; } to { opacity: 0; } } /* The gutter darkens under a leaf that is standing up over it */ .book__turning .book__spine { animation: gutter-deepen var.$trans-slow ease-in-out; } @keyframes gutter-deepen { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } } /* One page per view has no gutter to hinge on, so the page is dealt off the * pile in the direction of travel instead of pretending to be bound. */ .book__spread--single.book__turning--forward .page { animation: page-in-from-right var.$trans-mid ease-out; } .book__spread--single.book__turning--back .page { animation: page-in-from-left var.$trans-mid ease-out; } @keyframes page-in-from-right { from { transform: translateX(14%); opacity: 0.2; } to { transform: translateX(0); opacity: 1; } } @keyframes page-in-from-left { from { transform: translateX(-14%); opacity: 0.2; } to { transform: translateX(0); opacity: 1; } } /* Turn controls, tucked into the outer edge of each page */ .book__turner { position: absolute; top: 50%; transform: translateY(-50%); z-index: 5; display: flex; align-items: center; justify-content: center; width: 2.4rem; height: 3.4rem; color: color.$ink-faint; border-radius: var.$block-radius; transition: color var.$trans-quick, background-color var.$trans-quick; &:hover:not(:disabled) { color: color.$ribbon; background-color: rgba(140, 112, 74, 0.08); } &:disabled { opacity: 0.25; cursor: default; } } .book__turner--back { left: 0.1rem; } .book__turner--forward { right: 0.1rem; } /* The ribbon, hanging out of the gutter. Marks the page being written on. */ .book__ribbon { position: absolute; top: 0; left: calc(50% - 0.55rem); width: 1.1rem; z-index: 4; pointer-events: none; background-image: linear-gradient(to right, color.$ribbon-deep, color.$ribbon 45%, color.$ribbon-deep); box-shadow: 0 0 0.4rem rgba(20, 14, 9, 0.3); transition: height var.$trans-mid ease-out, opacity var.$trans-quick; &::after { content: ""; position: absolute; bottom: -0.7rem; left: 0; width: 100%; height: 0.75rem; background-color: color.$ribbon; clip-path: polygon(0 0, 100% 0, 100% 100%, 50% 55%, 0 100%); } }