Skip to content

Commit f968464

Browse files
committed
feat(design): apply libretto design language to quote generator
Introduces Libretto: an opera-programme aesthetic — oxblood velvet ground, swagged crimson stage curtain with gold tassels, parchment program pages framed in gilt, Playfair Display italic for wordmarks, Cinzel inscribed capitals, EB Garamond body. Signature moves: filigree corner ornaments, fleuron dividers, footlight shimmer bar at viewport bottom, damask dot backdrop. Applies it to /apps/quote-generator: the page becomes a two-act theatre programme (Act I lobby top-bar, Act II 'The Composition' hosting the quote generator inside a gilt-trimmed parchment card). The embedded ControlPanel is reskinned via scoped CSS overrides — velvet panels, gilt borders, Cinzel section heads, parchment text. Download buttons become gilt 'curtain tickets'. Cinzel loaded site-wide via index.html. Adds four new canvas output themes to the quote generator: libretto (parchment with gilt filigree corners + crimson curtain swag + fleuron), chalkboard (slate green with chalk dust, dashed border, star doodle and scribble), tarot (deep violet with gold ornate border, sun/moon glyphs, corner rosettes and mystic dots), ransom (cut-paper strips in aged cream shades rotated at angles over speckled paper). Matching presets wired into the theme grid. Adds the Libretto card to /design — inner grid set back to lg-cols-3. Removes the old 'Atmospheric Soundtrack' music player section from the page.
1 parent 0375dc6 commit f968464

5 files changed

Lines changed: 773 additions & 43 deletions

File tree

public/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<link href="https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400..800;1,400..800&display=swap" rel="stylesheet">
4040
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght,SOFT,WONK@0,9..144,200..900,0..100,0..1;1,9..144,200..900,0..100,0..1&display=swap" rel="stylesheet">
4141
<link href="https://fonts.googleapis.com/css2?family=Abril+Fatface&display=swap" rel="stylesheet">
42+
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;800&display=swap" rel="stylesheet">
4243
<title>fezcodex</title>
4344
</head>
4445
<body class="bg-slate-950">

src/app/QuoteGenerator/components/CanvasPreview.jsx

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,208 @@ const CanvasPreview = ({
558558
drawPaw(width*0.15, height*0.85, -Math.PI/6, 3);
559559
drawPaw(width*0.5, height*0.9, Math.PI/12, 1.2);
560560

561+
ctx.restore();
562+
} else if (themeType === 'libretto') {
563+
// Opera program — velvet curtain top, gilt fleuron, parchment center
564+
ctx.save();
565+
// Gilt hairline double frame
566+
const inset = Math.min(width, height) * 0.035;
567+
ctx.strokeStyle = '#C8A255';
568+
ctx.lineWidth = 2;
569+
ctx.strokeRect(inset, inset, width - inset * 2, height - inset * 2);
570+
ctx.lineWidth = 0.8;
571+
ctx.strokeRect(inset + 6, inset + 6, width - inset * 2 - 12, height - inset * 2 - 12);
572+
573+
// Top curtain swag — crimson
574+
const cH = height * 0.08;
575+
const curtainGrad = ctx.createLinearGradient(0, inset, 0, inset + cH);
576+
curtainGrad.addColorStop(0, '#7E1A24');
577+
curtainGrad.addColorStop(1, '#5A0F18');
578+
ctx.fillStyle = curtainGrad;
579+
for (let i = 0; i < 8; i++) {
580+
const segW = (width - inset * 2) / 8;
581+
const x = inset + i * segW;
582+
ctx.beginPath();
583+
ctx.moveTo(x, inset);
584+
ctx.quadraticCurveTo(x + segW / 2, inset + cH * (0.8 + (i % 2) * 0.2), x + segW, inset);
585+
ctx.closePath();
586+
ctx.fill();
587+
// gilt tassel
588+
ctx.fillStyle = '#C8A255';
589+
ctx.beginPath();
590+
ctx.arc(x + segW / 2, inset + cH * (0.8 + (i % 2) * 0.2) - 2, 3, 0, Math.PI * 2);
591+
ctx.fill();
592+
ctx.fillStyle = curtainGrad;
593+
}
594+
595+
// Gilt filigree corners (quarter-circle flourish)
596+
ctx.strokeStyle = '#C8A255';
597+
ctx.lineWidth = 1.5;
598+
const drawFlourish = (cx, cy, flip) => {
599+
ctx.save();
600+
ctx.translate(cx, cy);
601+
ctx.scale(flip, 1);
602+
ctx.beginPath();
603+
ctx.moveTo(0, 0);
604+
ctx.quadraticCurveTo(30, 0, 36, 30);
605+
ctx.moveTo(0, 0);
606+
ctx.quadraticCurveTo(0, 30, 30, 36);
607+
ctx.stroke();
608+
ctx.beginPath();
609+
ctx.arc(14, 14, 2.5, 0, Math.PI * 2);
610+
ctx.fillStyle = '#C8A255';
611+
ctx.fill();
612+
ctx.restore();
613+
};
614+
drawFlourish(inset + 14, inset + cH + 14, 1);
615+
drawFlourish(width - inset - 14, inset + cH + 14, -1);
616+
drawFlourish(inset + 14, height - inset - 14, 1);
617+
drawFlourish(width - inset - 14, height - inset - 14, -1);
618+
619+
// Fleuron centered near top, just below curtain
620+
const fY = inset + cH + 22;
621+
ctx.fillStyle = '#C8A255';
622+
ctx.beginPath();
623+
ctx.moveTo(width / 2, fY - 8);
624+
ctx.quadraticCurveTo(width / 2 - 12, fY, width / 2 - 18, fY);
625+
ctx.quadraticCurveTo(width / 2 - 12, fY, width / 2, fY + 8);
626+
ctx.quadraticCurveTo(width / 2 + 12, fY, width / 2 + 18, fY);
627+
ctx.quadraticCurveTo(width / 2 + 12, fY, width / 2, fY - 8);
628+
ctx.fill();
629+
630+
ctx.restore();
631+
} else if (themeType === 'chalkboard') {
632+
// Green chalkboard — slate texture + chalk dust
633+
ctx.save();
634+
// Chalk dust speckles
635+
ctx.globalAlpha = 0.12;
636+
ctx.fillStyle = '#ffffff';
637+
for (let i = 0; i < 900; i++) {
638+
const x = Math.random() * width;
639+
const y = Math.random() * height;
640+
const r = Math.random() * 1.2;
641+
ctx.beginPath();
642+
ctx.arc(x, y, r, 0, Math.PI * 2);
643+
ctx.fill();
644+
}
645+
ctx.globalAlpha = 1;
646+
// Chalk border
647+
ctx.strokeStyle = 'rgba(255,255,255,0.55)';
648+
ctx.lineWidth = 3;
649+
ctx.setLineDash([10, 6]);
650+
ctx.strokeRect(40, 40, width - 80, height - 80);
651+
ctx.setLineDash([]);
652+
// Chalk corner doodle — a star
653+
ctx.strokeStyle = 'rgba(255,255,255,0.4)';
654+
ctx.lineWidth = 2;
655+
const drawStar = (cx, cy, r) => {
656+
ctx.beginPath();
657+
for (let i = 0; i < 5; i++) {
658+
const a1 = -Math.PI / 2 + (i * Math.PI * 2) / 5;
659+
const a2 = a1 + Math.PI / 5;
660+
ctx.lineTo(cx + Math.cos(a1) * r, cy + Math.sin(a1) * r);
661+
ctx.lineTo(cx + Math.cos(a2) * (r * 0.45), cy + Math.sin(a2) * (r * 0.45));
662+
}
663+
ctx.closePath();
664+
ctx.stroke();
665+
};
666+
drawStar(width - 100, 100, 30);
667+
// Underline scribble near bottom center
668+
ctx.beginPath();
669+
ctx.moveTo(width * 0.3, height - 100);
670+
ctx.quadraticCurveTo(width * 0.5, height - 90, width * 0.7, height - 100);
671+
ctx.stroke();
672+
ctx.restore();
673+
} else if (themeType === 'tarot') {
674+
// Tarot card — gold ornate border, sun/moon symbols, mystic dots
675+
ctx.save();
676+
// Double gold border
677+
ctx.strokeStyle = '#D4A94A';
678+
ctx.lineWidth = 8;
679+
ctx.strokeRect(30, 30, width - 60, height - 60);
680+
ctx.lineWidth = 2;
681+
ctx.strokeRect(48, 48, width - 96, height - 96);
682+
// Corner rosettes
683+
const rosette = (cx, cy) => {
684+
ctx.save();
685+
ctx.translate(cx, cy);
686+
ctx.fillStyle = '#D4A94A';
687+
for (let i = 0; i < 8; i++) {
688+
ctx.rotate((Math.PI * 2) / 8);
689+
ctx.beginPath();
690+
ctx.ellipse(0, -12, 3, 8, 0, 0, Math.PI * 2);
691+
ctx.fill();
692+
}
693+
ctx.beginPath();
694+
ctx.arc(0, 0, 4, 0, Math.PI * 2);
695+
ctx.fill();
696+
ctx.restore();
697+
};
698+
rosette(70, 70);
699+
rosette(width - 70, 70);
700+
rosette(70, height - 70);
701+
rosette(width - 70, height - 70);
702+
703+
// Sun top-center
704+
ctx.fillStyle = '#E8C878';
705+
ctx.beginPath();
706+
ctx.arc(width / 2, 100, 18, 0, Math.PI * 2);
707+
ctx.fill();
708+
ctx.strokeStyle = '#E8C878';
709+
ctx.lineWidth = 2;
710+
for (let i = 0; i < 12; i++) {
711+
const a = (i * Math.PI * 2) / 12;
712+
ctx.beginPath();
713+
ctx.moveTo(width / 2 + Math.cos(a) * 24, 100 + Math.sin(a) * 24);
714+
ctx.lineTo(width / 2 + Math.cos(a) * 34, 100 + Math.sin(a) * 34);
715+
ctx.stroke();
716+
}
717+
// Moon bottom-center (crescent)
718+
ctx.fillStyle = '#E8C878';
719+
ctx.beginPath();
720+
ctx.arc(width / 2, height - 100, 16, 0, Math.PI * 2);
721+
ctx.fill();
722+
ctx.fillStyle = backgroundColor;
723+
ctx.beginPath();
724+
ctx.arc(width / 2 - 6, height - 100, 14, 0, Math.PI * 2);
725+
ctx.fill();
726+
// Mystic dots scattered
727+
ctx.fillStyle = 'rgba(232,200,120,0.35)';
728+
for (let i = 0; i < 40; i++) {
729+
const x = 60 + Math.random() * (width - 120);
730+
const y = 140 + Math.random() * (height - 280);
731+
const r = Math.random() * 1.8;
732+
ctx.beginPath();
733+
ctx.arc(x, y, r, 0, Math.PI * 2);
734+
ctx.fill();
735+
}
736+
ctx.restore();
737+
} else if (themeType === 'ransom') {
738+
// Ransom note — torn paper rects with different text colors and rotations
739+
ctx.save();
740+
ctx.globalAlpha = 0.08;
741+
ctx.fillStyle = '#000';
742+
for (let i = 0; i < 1600; i++) {
743+
const x = Math.random() * width;
744+
const y = Math.random() * height;
745+
ctx.fillRect(x, y, 1.2, 1.2);
746+
}
747+
ctx.globalAlpha = 1;
748+
// Random torn paper strips behind
749+
const colors = ['#F5E8C9', '#E8D5B0', '#F0E0C0', '#D9C79E'];
750+
for (let i = 0; i < 18; i++) {
751+
const w = 70 + Math.random() * 140;
752+
const h = 50 + Math.random() * 90;
753+
const x = Math.random() * (width - w);
754+
const y = Math.random() * (height - h);
755+
ctx.save();
756+
ctx.translate(x + w / 2, y + h / 2);
757+
ctx.rotate((Math.random() - 0.5) * 0.4);
758+
ctx.fillStyle = colors[i % colors.length];
759+
ctx.globalAlpha = 0.25;
760+
ctx.fillRect(-w / 2, -h / 2, w, h);
761+
ctx.restore();
762+
}
561763
ctx.restore();
562764
}
563765

src/app/QuoteGenerator/components/ControlPanel.jsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,62 @@ const THEME_PRESETS = [
393393
overlayOpacity: 0,
394394
},
395395
},
396+
{
397+
name: 'Libretto',
398+
config: {
399+
fontFamily: 'Playfair Display',
400+
backgroundType: 'radial',
401+
backgroundColor: '#F0E4C8',
402+
gradientColor1: '#F0E4C8',
403+
gradientColor2: '#E8D9B5',
404+
textColor: '#1A0A0D',
405+
fontWeight: 400,
406+
themeType: 'libretto',
407+
textAlign: 'center',
408+
overlayOpacity: 0,
409+
},
410+
},
411+
{
412+
name: 'Chalkboard',
413+
config: {
414+
fontFamily: 'Caveat',
415+
backgroundType: 'solid',
416+
backgroundColor: '#1F3228',
417+
textColor: '#F5F1E3',
418+
fontWeight: 400,
419+
themeType: 'chalkboard',
420+
textAlign: 'center',
421+
overlayOpacity: 0,
422+
},
423+
},
424+
{
425+
name: 'Tarot',
426+
config: {
427+
fontFamily: 'Cinzel',
428+
backgroundType: 'radial',
429+
backgroundColor: '#1A0A2E',
430+
gradientColor1: '#2D1B4F',
431+
gradientColor2: '#0F0520',
432+
textColor: '#E8C878',
433+
fontWeight: 700,
434+
themeType: 'tarot',
435+
textAlign: 'center',
436+
overlayOpacity: 0,
437+
},
438+
},
439+
{
440+
name: 'Ransom',
441+
config: {
442+
fontFamily: 'Impact',
443+
backgroundType: 'solid',
444+
backgroundColor: '#F5E8C9',
445+
textColor: '#0A0A0A',
446+
fontWeight: 900,
447+
themeType: 'ransom',
448+
textAlign: 'center',
449+
overlayOpacity: 0,
450+
},
451+
},
396452
];
397453

398454
const ControlPanel = ({ state, updateState }) => {

0 commit comments

Comments
 (0)