@cube-drone/marquee-html-renderer 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/render.js +79 -9
- package/package.json +5 -5
- package/src/render.ts +85 -8
package/dist/render.js
CHANGED
|
@@ -158,7 +158,7 @@ export function usedFontTokens(html) {
|
|
|
158
158
|
}
|
|
159
159
|
/** One rung of the font-element seven-step dial: presentational floor
|
|
160
160
|
* (works with no stylesheet, under any CSP), stylesheet class as ceiling.
|
|
161
|
-
* The named rungs -
|
|
161
|
+
* The named rungs - teeny, tiny, huge, enormous - are unnecessary
|
|
162
162
|
* given the dial, and yet. */
|
|
163
163
|
function sizeRung(value, inner) {
|
|
164
164
|
return `<font class="mq-size-${value}" size="${value}">${inner}</font>`;
|
|
@@ -404,7 +404,7 @@ function span(name, attrs, nodes, ctx) {
|
|
|
404
404
|
}
|
|
405
405
|
return inner; // not on the list: the words survive in their own clothes
|
|
406
406
|
}
|
|
407
|
-
case "
|
|
407
|
+
case "teeny":
|
|
408
408
|
return sizeRung("1", inner);
|
|
409
409
|
case "tiny":
|
|
410
410
|
return sizeRung("2", inner);
|
|
@@ -419,7 +419,12 @@ function span(name, attrs, nodes, ctx) {
|
|
|
419
419
|
}
|
|
420
420
|
return inner; // invalid value: the effect degrades, the words survive
|
|
421
421
|
}
|
|
422
|
-
case "sidenote":
|
|
422
|
+
case "sidenote":
|
|
423
|
+
case "aside":
|
|
424
|
+
case "footnote": {
|
|
425
|
+
// Permanent synonyms (SPEC.md): nobody keeps these three words
|
|
426
|
+
// distinct in their head, and an aside that silently isn't an aside
|
|
427
|
+
// is worse than two extra names - the list-marker rule, one layer up.
|
|
423
428
|
// A numbered mark in the flow; the note itself flushes just below the
|
|
424
429
|
// triggering paragraph (see flushNotes). Numbering runs sequentially
|
|
425
430
|
// through the whole document.
|
|
@@ -438,18 +443,41 @@ function span(name, attrs, nodes, ctx) {
|
|
|
438
443
|
const rate = attrs["rate"] !== undefined && COUNT.test(attrs["rate"])
|
|
439
444
|
? ` style="--mq-rate:${attrs["rate"]}"`
|
|
440
445
|
: "";
|
|
446
|
+
if (attrs["by"] === "letter" || attrs["by"] === "word") {
|
|
447
|
+
// Split blink: ramp is theater-marquee chase lights, scatter is
|
|
448
|
+
// twinkle. The rate var rides the container; units inherit it.
|
|
449
|
+
return bySegments(name, attrs["by"], attrs["phase"], nodes, ctx, rate);
|
|
450
|
+
}
|
|
441
451
|
return `<span class="mq-blink"${rate}>${inner}</span>`;
|
|
442
452
|
}
|
|
443
453
|
case "rainbow":
|
|
444
454
|
case "bounce":
|
|
445
455
|
case "jitter":
|
|
446
456
|
case "wave":
|
|
457
|
+
case "rubber":
|
|
447
458
|
if (attrs["by"] === "letter" || attrs["by"] === "word") {
|
|
448
459
|
return bySegments(name, attrs["by"], attrs["phase"], nodes, ctx);
|
|
449
460
|
}
|
|
450
461
|
return `<span class="mq-${name}">${inner}</span>`;
|
|
451
|
-
case "typewriter":
|
|
452
|
-
|
|
462
|
+
case "typewriter": {
|
|
463
|
+
// Inherently per-unit: the reveal IS a by=letter effect (by=word for
|
|
464
|
+
// word-at-a-time). speed= is units per second; the container carries
|
|
465
|
+
// the per-unit delay step, each unit its ordinal in --mq-o.
|
|
466
|
+
const by = attrs["by"] === "word" ? "word" : "letter";
|
|
467
|
+
const step = revealStep(attrs["speed"], 14);
|
|
468
|
+
return bySegments(name, by, attrs["phase"], nodes, ctx, ` style="--mq-tw-step:${step}s"`);
|
|
469
|
+
}
|
|
470
|
+
case "fadein": {
|
|
471
|
+
// The ghostly reveal. Bare [fadein] fades the whole run in once;
|
|
472
|
+
// by=letter / by=word drift units in on staggered starts (same
|
|
473
|
+
// one-shot family as typewriter: sequential ordinals, high cap);
|
|
474
|
+
// phase=scatter is apparition weather.
|
|
475
|
+
if (attrs["by"] === "letter" || attrs["by"] === "word") {
|
|
476
|
+
const step = revealStep(attrs["speed"], 16);
|
|
477
|
+
return bySegments(name, attrs["by"], attrs["phase"], nodes, ctx, ` style="--mq-fi-step:${step}s"`);
|
|
478
|
+
}
|
|
479
|
+
return `<span class="mq-fadein">${inner}</span>`;
|
|
480
|
+
}
|
|
453
481
|
}
|
|
454
482
|
return inner; // unknown span: pure shrug, children as plain content
|
|
455
483
|
}
|
|
@@ -461,9 +489,13 @@ const SEGMENTERS = {
|
|
|
461
489
|
letter: new Intl.Segmenter("en", { granularity: "grapheme" }),
|
|
462
490
|
word: new Intl.Segmenter("en", { granularity: "word" }),
|
|
463
491
|
};
|
|
464
|
-
/** DOM-weight discipline: past this many units, the run animates whole.
|
|
492
|
+
/** DOM-weight discipline: past this many units, the run animates whole.
|
|
493
|
+
* Loopers pay a live animation per element forever, so they cap low;
|
|
494
|
+
* typewriter's units are 1ms one-shots (finished animations cost nothing)
|
|
495
|
+
* and its natural material is long text, so it caps high. */
|
|
465
496
|
const MAX_SPLIT_UNITS = 400;
|
|
466
|
-
|
|
497
|
+
const MAX_REVEAL_UNITS = 2000;
|
|
498
|
+
function bySegments(effect, by, phaseAttr, nodes, ctx, containerStyle = "") {
|
|
467
499
|
// Each effect has a natural phase order (jitter scatters, the rest sweep);
|
|
468
500
|
// the knob overrides it either way. Invalid values degrade to the default.
|
|
469
501
|
const phase = phaseAttr === "scatter" || phaseAttr === "ramp"
|
|
@@ -473,10 +505,11 @@ function bySegments(effect, by, phaseAttr, nodes, ctx) {
|
|
|
473
505
|
: "ramp";
|
|
474
506
|
const state = { effect, by, phase, i: 0, total: 0 };
|
|
475
507
|
state.total = countUnits(nodes, by);
|
|
476
|
-
|
|
508
|
+
const cap = isReveal(effect) ? MAX_REVEAL_UNITS : MAX_SPLIT_UNITS;
|
|
509
|
+
if (state.total === 0 || state.total > cap) {
|
|
477
510
|
return `<span class="mq-${effect}">${children(nodes, ctx)}</span>`;
|
|
478
511
|
}
|
|
479
|
-
return `<span class="mq-${effect} mq-split">${splitRender(nodes, ctx, state)}</span>`;
|
|
512
|
+
return `<span class="mq-${effect} mq-split"${containerStyle}>${splitRender(nodes, ctx, state)}</span>`;
|
|
480
513
|
}
|
|
481
514
|
/** A segment gets wrapped if it's animatable: for words, word-like segments
|
|
482
515
|
* (spaces and bare punctuation ride along); for letters, anything that
|
|
@@ -488,6 +521,19 @@ function isUnit(seg, by) {
|
|
|
488
521
|
* twice) in both phase orders: ramp sweeps, scatter scrambles by a fixed
|
|
489
522
|
* integer hash - randomness-shaped, never random. */
|
|
490
523
|
function unitOffset(state) {
|
|
524
|
+
// Reveal offsets (typewriter, fadein) are sequential INTEGERS (the
|
|
525
|
+
// ordinal; delay = ordinal x step), unlike the cyclic 0..1 fractions the
|
|
526
|
+
// looping effects replay. phase=scatter reveals in a
|
|
527
|
+
// scrambled-but-deterministic order: a stride at the run's golden-ratio
|
|
528
|
+
// point, nudged coprime with the total, walks a well-spread permutation
|
|
529
|
+
// at EVERY length. (A fixed prime stride is a trap: 7919 mod 40 = 39 =
|
|
530
|
+
// -1, so forty-unit runs typed in backwards.)
|
|
531
|
+
if (isReveal(state.effect)) {
|
|
532
|
+
const o = state.phase === "scatter"
|
|
533
|
+
? (state.i * scatterStride(state.total)) % state.total
|
|
534
|
+
: state.i;
|
|
535
|
+
return String(o);
|
|
536
|
+
}
|
|
491
537
|
let o;
|
|
492
538
|
if (state.phase === "scatter") {
|
|
493
539
|
o = ((state.i * 7919) % 101) / 101;
|
|
@@ -510,6 +556,30 @@ function unitOffset(state) {
|
|
|
510
556
|
}
|
|
511
557
|
return String(Math.round(o * 1000) / 1000);
|
|
512
558
|
}
|
|
559
|
+
/** speed= (units per second, a COUNT) into a per-unit delay step in
|
|
560
|
+
* seconds; invalid or absent falls to the effect's default rate. */
|
|
561
|
+
function revealStep(speedAttr, dflt) {
|
|
562
|
+
const speed = speedAttr !== undefined && COUNT.test(speedAttr) && Number(speedAttr) > 0
|
|
563
|
+
? Number(speedAttr)
|
|
564
|
+
: dflt;
|
|
565
|
+
return Math.round(1000 / speed) / 1000;
|
|
566
|
+
}
|
|
567
|
+
/** The one-shot reveals: sequential ordinals, the high unit cap. */
|
|
568
|
+
function isReveal(effect) {
|
|
569
|
+
return effect === "typewriter" || effect === "fadein";
|
|
570
|
+
}
|
|
571
|
+
function gcd(a, b) {
|
|
572
|
+
return b === 0 ? a : gcd(b, a % b);
|
|
573
|
+
}
|
|
574
|
+
/** The smallest stride >= ~61.8% of total that's coprime with it (falls
|
|
575
|
+
* back to 1 for degenerate totals - a 1- or 2-unit "scramble" is fate). */
|
|
576
|
+
function scatterStride(total) {
|
|
577
|
+
let stride = Math.max(1, Math.round(total * 0.618));
|
|
578
|
+
while (stride < total && gcd(stride, total) !== 1) {
|
|
579
|
+
stride += 1;
|
|
580
|
+
}
|
|
581
|
+
return gcd(stride, total) === 1 ? stride : 1;
|
|
582
|
+
}
|
|
513
583
|
function countUnits(nodes, by) {
|
|
514
584
|
let n = 0;
|
|
515
585
|
for (const node of nodes) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cube-drone/marquee-html-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Reference static HTML renderer for the Marquee markup language",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"prepack": "npm run build"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@cube-drone/marquee-parser": "^0.
|
|
38
|
+
"@cube-drone/marquee-parser": "^0.2.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@cube-drone/marquee-css": "^0.
|
|
42
|
-
"@cube-drone/marquee-fonts": "^0.
|
|
43
|
-
"@cube-drone/marquee-turbolink": "^0.
|
|
41
|
+
"@cube-drone/marquee-css": "^0.2.0",
|
|
42
|
+
"@cube-drone/marquee-fonts": "^0.2.0",
|
|
43
|
+
"@cube-drone/marquee-turbolink": "^0.2.0",
|
|
44
44
|
"@types/node": "^24.0.0",
|
|
45
45
|
"typescript": "^5.9.0"
|
|
46
46
|
}
|
package/src/render.ts
CHANGED
|
@@ -180,7 +180,7 @@ export function usedFontTokens(html: string): string[] {
|
|
|
180
180
|
|
|
181
181
|
/** One rung of the font-element seven-step dial: presentational floor
|
|
182
182
|
* (works with no stylesheet, under any CSP), stylesheet class as ceiling.
|
|
183
|
-
* The named rungs -
|
|
183
|
+
* The named rungs - teeny, tiny, huge, enormous - are unnecessary
|
|
184
184
|
* given the dial, and yet. */
|
|
185
185
|
function sizeRung(value: string, inner: string): string {
|
|
186
186
|
return `<font class="mq-size-${value}" size="${value}">${inner}</font>`;
|
|
@@ -440,7 +440,7 @@ function span(name: string, attrs: Attrs, nodes: Node[], ctx: Ctx): string {
|
|
|
440
440
|
}
|
|
441
441
|
return inner; // not on the list: the words survive in their own clothes
|
|
442
442
|
}
|
|
443
|
-
case "
|
|
443
|
+
case "teeny":
|
|
444
444
|
return sizeRung("1", inner);
|
|
445
445
|
case "tiny":
|
|
446
446
|
return sizeRung("2", inner);
|
|
@@ -455,7 +455,12 @@ function span(name: string, attrs: Attrs, nodes: Node[], ctx: Ctx): string {
|
|
|
455
455
|
}
|
|
456
456
|
return inner; // invalid value: the effect degrades, the words survive
|
|
457
457
|
}
|
|
458
|
-
case "sidenote":
|
|
458
|
+
case "sidenote":
|
|
459
|
+
case "aside":
|
|
460
|
+
case "footnote": {
|
|
461
|
+
// Permanent synonyms (SPEC.md): nobody keeps these three words
|
|
462
|
+
// distinct in their head, and an aside that silently isn't an aside
|
|
463
|
+
// is worse than two extra names - the list-marker rule, one layer up.
|
|
459
464
|
// A numbered mark in the flow; the note itself flushes just below the
|
|
460
465
|
// triggering paragraph (see flushNotes). Numbering runs sequentially
|
|
461
466
|
// through the whole document.
|
|
@@ -474,18 +479,41 @@ function span(name: string, attrs: Attrs, nodes: Node[], ctx: Ctx): string {
|
|
|
474
479
|
const rate = attrs["rate"] !== undefined && COUNT.test(attrs["rate"])
|
|
475
480
|
? ` style="--mq-rate:${attrs["rate"]}"`
|
|
476
481
|
: "";
|
|
482
|
+
if (attrs["by"] === "letter" || attrs["by"] === "word") {
|
|
483
|
+
// Split blink: ramp is theater-marquee chase lights, scatter is
|
|
484
|
+
// twinkle. The rate var rides the container; units inherit it.
|
|
485
|
+
return bySegments(name, attrs["by"], attrs["phase"], nodes, ctx, rate);
|
|
486
|
+
}
|
|
477
487
|
return `<span class="mq-blink"${rate}>${inner}</span>`;
|
|
478
488
|
}
|
|
479
489
|
case "rainbow":
|
|
480
490
|
case "bounce":
|
|
481
491
|
case "jitter":
|
|
482
492
|
case "wave":
|
|
493
|
+
case "rubber":
|
|
483
494
|
if (attrs["by"] === "letter" || attrs["by"] === "word") {
|
|
484
495
|
return bySegments(name, attrs["by"], attrs["phase"], nodes, ctx);
|
|
485
496
|
}
|
|
486
497
|
return `<span class="mq-${name}">${inner}</span>`;
|
|
487
|
-
case "typewriter":
|
|
488
|
-
|
|
498
|
+
case "typewriter": {
|
|
499
|
+
// Inherently per-unit: the reveal IS a by=letter effect (by=word for
|
|
500
|
+
// word-at-a-time). speed= is units per second; the container carries
|
|
501
|
+
// the per-unit delay step, each unit its ordinal in --mq-o.
|
|
502
|
+
const by = attrs["by"] === "word" ? "word" : "letter";
|
|
503
|
+
const step = revealStep(attrs["speed"], 14);
|
|
504
|
+
return bySegments(name, by, attrs["phase"], nodes, ctx, ` style="--mq-tw-step:${step}s"`);
|
|
505
|
+
}
|
|
506
|
+
case "fadein": {
|
|
507
|
+
// The ghostly reveal. Bare [fadein] fades the whole run in once;
|
|
508
|
+
// by=letter / by=word drift units in on staggered starts (same
|
|
509
|
+
// one-shot family as typewriter: sequential ordinals, high cap);
|
|
510
|
+
// phase=scatter is apparition weather.
|
|
511
|
+
if (attrs["by"] === "letter" || attrs["by"] === "word") {
|
|
512
|
+
const step = revealStep(attrs["speed"], 16);
|
|
513
|
+
return bySegments(name, attrs["by"], attrs["phase"], nodes, ctx, ` style="--mq-fi-step:${step}s"`);
|
|
514
|
+
}
|
|
515
|
+
return `<span class="mq-fadein">${inner}</span>`;
|
|
516
|
+
}
|
|
489
517
|
}
|
|
490
518
|
return inner; // unknown span: pure shrug, children as plain content
|
|
491
519
|
}
|
|
@@ -502,8 +530,12 @@ const SEGMENTERS = {
|
|
|
502
530
|
|
|
503
531
|
type SplitBy = keyof typeof SEGMENTERS;
|
|
504
532
|
|
|
505
|
-
/** DOM-weight discipline: past this many units, the run animates whole.
|
|
533
|
+
/** DOM-weight discipline: past this many units, the run animates whole.
|
|
534
|
+
* Loopers pay a live animation per element forever, so they cap low;
|
|
535
|
+
* typewriter's units are 1ms one-shots (finished animations cost nothing)
|
|
536
|
+
* and its natural material is long text, so it caps high. */
|
|
506
537
|
const MAX_SPLIT_UNITS = 400;
|
|
538
|
+
const MAX_REVEAL_UNITS = 2000;
|
|
507
539
|
|
|
508
540
|
type Phase = "ramp" | "scatter";
|
|
509
541
|
|
|
@@ -521,6 +553,7 @@ function bySegments(
|
|
|
521
553
|
phaseAttr: string | undefined,
|
|
522
554
|
nodes: Node[],
|
|
523
555
|
ctx: Ctx,
|
|
556
|
+
containerStyle = "",
|
|
524
557
|
): string {
|
|
525
558
|
// Each effect has a natural phase order (jitter scatters, the rest sweep);
|
|
526
559
|
// the knob overrides it either way. Invalid values degrade to the default.
|
|
@@ -532,10 +565,11 @@ function bySegments(
|
|
|
532
565
|
: "ramp";
|
|
533
566
|
const state: SplitState = { effect, by, phase, i: 0, total: 0 };
|
|
534
567
|
state.total = countUnits(nodes, by);
|
|
535
|
-
|
|
568
|
+
const cap = isReveal(effect) ? MAX_REVEAL_UNITS : MAX_SPLIT_UNITS;
|
|
569
|
+
if (state.total === 0 || state.total > cap) {
|
|
536
570
|
return `<span class="mq-${effect}">${children(nodes, ctx)}</span>`;
|
|
537
571
|
}
|
|
538
|
-
return `<span class="mq-${effect} mq-split">${splitRender(nodes, ctx, state)}</span>`;
|
|
572
|
+
return `<span class="mq-${effect} mq-split"${containerStyle}>${splitRender(nodes, ctx, state)}</span>`;
|
|
539
573
|
}
|
|
540
574
|
|
|
541
575
|
/** A segment gets wrapped if it's animatable: for words, word-like segments
|
|
@@ -549,6 +583,20 @@ function isUnit(seg: Intl.SegmentData, by: SplitBy): boolean {
|
|
|
549
583
|
* twice) in both phase orders: ramp sweeps, scatter scrambles by a fixed
|
|
550
584
|
* integer hash - randomness-shaped, never random. */
|
|
551
585
|
function unitOffset(state: SplitState): string {
|
|
586
|
+
// Reveal offsets (typewriter, fadein) are sequential INTEGERS (the
|
|
587
|
+
// ordinal; delay = ordinal x step), unlike the cyclic 0..1 fractions the
|
|
588
|
+
// looping effects replay. phase=scatter reveals in a
|
|
589
|
+
// scrambled-but-deterministic order: a stride at the run's golden-ratio
|
|
590
|
+
// point, nudged coprime with the total, walks a well-spread permutation
|
|
591
|
+
// at EVERY length. (A fixed prime stride is a trap: 7919 mod 40 = 39 =
|
|
592
|
+
// -1, so forty-unit runs typed in backwards.)
|
|
593
|
+
if (isReveal(state.effect)) {
|
|
594
|
+
const o =
|
|
595
|
+
state.phase === "scatter"
|
|
596
|
+
? (state.i * scatterStride(state.total)) % state.total
|
|
597
|
+
: state.i;
|
|
598
|
+
return String(o);
|
|
599
|
+
}
|
|
552
600
|
let o: number;
|
|
553
601
|
if (state.phase === "scatter") {
|
|
554
602
|
o = ((state.i * 7919) % 101) / 101;
|
|
@@ -571,6 +619,35 @@ function unitOffset(state: SplitState): string {
|
|
|
571
619
|
return String(Math.round(o * 1000) / 1000);
|
|
572
620
|
}
|
|
573
621
|
|
|
622
|
+
/** speed= (units per second, a COUNT) into a per-unit delay step in
|
|
623
|
+
* seconds; invalid or absent falls to the effect's default rate. */
|
|
624
|
+
function revealStep(speedAttr: string | undefined, dflt: number): number {
|
|
625
|
+
const speed =
|
|
626
|
+
speedAttr !== undefined && COUNT.test(speedAttr) && Number(speedAttr) > 0
|
|
627
|
+
? Number(speedAttr)
|
|
628
|
+
: dflt;
|
|
629
|
+
return Math.round(1000 / speed) / 1000;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** The one-shot reveals: sequential ordinals, the high unit cap. */
|
|
633
|
+
function isReveal(effect: string): boolean {
|
|
634
|
+
return effect === "typewriter" || effect === "fadein";
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function gcd(a: number, b: number): number {
|
|
638
|
+
return b === 0 ? a : gcd(b, a % b);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** The smallest stride >= ~61.8% of total that's coprime with it (falls
|
|
642
|
+
* back to 1 for degenerate totals - a 1- or 2-unit "scramble" is fate). */
|
|
643
|
+
function scatterStride(total: number): number {
|
|
644
|
+
let stride = Math.max(1, Math.round(total * 0.618));
|
|
645
|
+
while (stride < total && gcd(stride, total) !== 1) {
|
|
646
|
+
stride += 1;
|
|
647
|
+
}
|
|
648
|
+
return gcd(stride, total) === 1 ? stride : 1;
|
|
649
|
+
}
|
|
650
|
+
|
|
574
651
|
function countUnits(nodes: Node[], by: SplitBy): number {
|
|
575
652
|
let n = 0;
|
|
576
653
|
for (const node of nodes) {
|