Download index.html from abeea/pretext: direct link, hf CLI and curl.
- Browser
- Download file 36.8 kB
-
https://huggingface.co/spaces/abeea/pretext/resolve/main/index.html
- Command line
-
hf download hf://spaces/abeea/pretext/index.html
-
curl -L -o index.html https://huggingface.co/spaces/abeea/pretext/resolve/main/index.html
36.8 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Pretext Project</title> | |
| <!-- Famst Agency SEO: start --> | |
| <meta name="description" content="Pretext Project is a Famst Agency web project by Abdullah Tarar. Famst Agency is an AI & Web Development Agency building websites, e-commerce demos, AI tools, automation, business systems, and SEO-ready digital products."> | |
| <meta name="keywords" content="Pretext Project, Pretext, Famst Agency, Famst Agency projects, Abdullah Tarar, Muhammad Abdullah Tarar, Abdullah Tarar developer, Famst Agency AI and Web Development Agency, AI web development agency, website development, custom web app, static website, Hugging Face Space, SEO ready website, Schema.org structured data, image SEO, video SEO, automation tools, ecommerce development, business website development, Pakistan web developer, Phalia Punjab Pakistan developer"> | |
| <meta name="author" content="Famst Agency, Abdullah Tarar, Muhammad Abdullah Tarar"> | |
| <meta name="creator" content="Abdullah Tarar"> | |
| <meta name="publisher" content="Famst Agency"> | |
| <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"> | |
| <meta name="googlebot" content="index, follow"> | |
| <meta name="bingbot" content="index, follow"> | |
| <meta name="theme-color" content="#0f766e"> | |
| <meta name="coverage" content="Worldwide"> | |
| <link rel="canonical" href="https://famstagency.com/projects/pretext/index.html"> | |
| <link rel="sitemap" type="application/xml" href="sitemap.xml"> | |
| <link rel="alternate" type="text/plain" href="llms.txt" title="LLMs.txt"> | |
| <meta property="og:type" content="website"> | |
| <meta property="og:site_name" content="Famst Agency Projects"> | |
| <meta property="og:title" content="Pretext Project | Famst Agency"> | |
| <meta property="og:description" content="Pretext Project is a Famst Agency web project by Abdullah Tarar. Famst Agency is an AI & Web Development Agency building websites, e-commerce demos, AI tools, automation, business systems, and SEO-ready digital products."> | |
| <meta property="og:url" content="https://famstagency.com/projects/pretext/index.html"> | |
| <meta property="og:image" content="https://i.ibb.co/mrpCb5Rd/abdullahtarar.jpg"> | |
| <meta property="og:image:alt" content="Pretext Project by Famst Agency and Abdullah Tarar"> | |
| <meta property="og:locale" content="en_US"> | |
| <meta name="twitter:card" content="summary_large_image"> | |
| <meta name="twitter:title" content="Pretext Project | Famst Agency"> | |
| <meta name="twitter:description" content="Pretext Project is a Famst Agency web project by Abdullah Tarar. Famst Agency is an AI & Web Development Agency building websites, e-commerce demos, AI tools, automation, business systems, and SEO-ready digital products."> | |
| <meta name="twitter:image" content="https://i.ibb.co/mrpCb5Rd/abdullahtarar.jpg"> | |
| <meta name="twitter:image:alt" content="Pretext Project by Famst Agency and Abdullah Tarar"> | |
| <script type="application/ld+json"> | |
| {"@context":"https://schema.org","@graph":[{"@type":["Organization","ProfessionalService"],"@id":"https://famstagency.com/#organization","name":"Famst Agency","alternateName":["Famst","Famst Digital Agency","Famst AI and Web Development Agency"],"url":"https://famstagency.com/","logo":"https://i.ibb.co/mrpCb5Rd/abdullahtarar.jpg","image":"https://i.ibb.co/mrpCb5Rd/abdullahtarar.jpg","description":"Famst Agency is an AI and web development agency founded by Abdullah Tarar. The agency builds websites, e-commerce stores, AI tools, automation, custom business systems, and search-ready digital projects.","slogan":"AI & Web Development Agency","founder":{"@id":"https://famstagency.com/abdulahtararlinktree.html#abdullah-tarar"},"sameAs":["https://www.instagram.com/famstagency/","http://facebook.com/famstagency","https://www.linkedin.com/company/famst-agency","https://x.com/famstagency","https://github.com/famstagency","https://www.youtube.com/@FamstAgency","https://www.tiktok.com/@famstagency"],"knowsAbout":["AI tools","web development","e-commerce development","automation","SEO","Schema.org","business websites","custom software"],"areaServed":["Pakistan","Worldwide"],"contactPoint":{"@type":"ContactPoint","telephone":"+923424844392","contactType":"customer service","areaServed":"Worldwide","availableLanguage":["English","Urdu"]}},{"@type":"Person","@id":"https://famstagency.com/abdulahtararlinktree.html#abdullah-tarar","name":"Abdullah Tarar","alternateName":["Muhammad Abdullah","Muhammad Abdullah Tarar"],"url":"https://famstagency.com/abdulahtararlinktree.html","jobTitle":"Developer and Founder of Famst Agency","worksFor":{"@id":"https://famstagency.com/#organization"},"sameAs":["https://famstagency.com/abdulahtararlinktree.html","https://www.instagram.com/abdullahtarar.3","https://www.linkedin.com/in/muhammadabdullahtarar"],"knowsAbout":["web development","AI tools","automation","e-commerce","SEO","business software","Schema.org structured data"],"description":"Abdullah Tarar is the developer and founder associated with Famst Agency, an AI and web development agency focused on websites, AI tools, automation, and business systems."},{"@type":"WebSite","@id":"https://famstagency.com/#website","url":"https://famstagency.com/","name":"Famst Agency","publisher":{"@id":"https://famstagency.com/#organization"},"inLanguage":"en"},{"@type":["WebPage","CreativeWork"],"@id":"https://famstagency.com/projects/pretext/index.html#webpage","url":"https://famstagency.com/projects/pretext/index.html","name":"Pretext Project","headline":"Pretext Project","description":"Pretext Project is a Famst Agency web project by Abdullah Tarar. Famst Agency is an AI & Web Development Agency building websites, e-commerce demos, AI tools, automation, business systems, and SEO-ready digital products.","keywords":["Pretext Project","Pretext","Famst Agency","Famst Agency projects","Abdullah Tarar","Muhammad Abdullah Tarar","Abdullah Tarar developer","Famst Agency AI and Web Development Agency","AI web development agency","website development","custom web app","static website","Hugging Face Space","SEO ready website","Schema.org structured data","image SEO","video SEO","automation tools","ecommerce development","business website development","Pakistan web developer","Phalia Punjab Pakistan developer"],"isPartOf":{"@id":"https://famstagency.com/#website"},"creator":{"@id":"https://famstagency.com/abdulahtararlinktree.html#abdullah-tarar"},"author":{"@id":"https://famstagency.com/abdulahtararlinktree.html#abdullah-tarar"},"publisher":{"@id":"https://famstagency.com/#organization"},"dateModified":"2026-07-10","inLanguage":"en","about":[{"@id":"https://famstagency.com/#organization"},{"@id":"https://famstagency.com/abdulahtararlinktree.html#abdullah-tarar"}],"mainEntityOfPage":"https://famstagency.com/projects/pretext/index.html","image":["https://i.ibb.co/mrpCb5Rd/abdullahtarar.jpg"]},{"@type":"BreadcrumbList","@id":"https://famstagency.com/projects/pretext/index.html#breadcrumbs","itemListElement":[{"@type":"ListItem","position":1,"name":"Famst Agency","item":"https://famstagency.com/"},{"@type":"ListItem","position":2,"name":"Pretext Project","item":"https://famstagency.com/projects/pretext/index.html"}]}]} | |
| </script> | |
| <!-- Famst Agency SEO: end --> | |
| <!-- Famst Agency Footer Styles: start --> | |
| <style> | |
| .famst-agency-footer{width:100%;margin-top:32px;padding:22px 16px;background:linear-gradient(135deg,#0f172a,#111827);color:#f8fafc;font:500 14px/1.55 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;clear:both;box-sizing:border-box;border-top:1px solid rgba(255,255,255,.12)} | |
| .famst-agency-footer__inner{max-width:1180px;margin:0 auto;display:grid;grid-template-columns:1.3fr .7fr;gap:18px;align-items:center} | |
| .famst-agency-footer p{margin:0;color:#e5e7eb}.famst-agency-footer strong{display:block;color:#fff;font-size:15px;margin-bottom:4px}.famst-agency-footer span{color:#cbd5e1}.famst-agency-footer a{color:#67e8f9;text-decoration:none;font-weight:800}.famst-agency-footer a:hover{text-decoration:underline}.famst-agency-footer__links{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.famst-agency-footer__links a{padding:6px 10px;border:1px solid rgba(103,232,249,.28);border-radius:999px;background:rgba(255,255,255,.06);font-size:12px} | |
| @media (max-width:700px){.famst-agency-footer__inner{grid-template-columns:1fr;text-align:center}.famst-agency-footer__links{justify-content:center}} | |
| </style> | |
| <!-- Famst Agency Footer Styles: end --> | |
| </head> | |
| <body> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=Syne+Mono&family=Space+Grotesk:wght@300;500;700&display=swap'); | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| :root{ | |
| --ink:#0d0d0d;--paper:#f7f5f0;--accent:#e84c1e;--accent2:#2563eb;--accent3:#16a34a; | |
| --mid:#888;--card:rgba(255,255,255,0.7);--border:rgba(0,0,0,0.08); | |
| --mono:'Syne Mono',monospace;--title:'Syne',sans-serif;--body:'Space Grotesk',sans-serif; | |
| } | |
| body{background:var(--paper);color:var(--ink);font-family:var(--body);overflow-x:hidden} | |
| @media(prefers-color-scheme:dark){ | |
| :root{--ink:#f0ede8;--paper:#111010;--accent:#ff6b3d;--accent2:#60a5fa;--accent3:#4ade80; | |
| --mid:#777;--card:rgba(30,28,26,0.8);--border:rgba(255,255,255,0.08);} | |
| } | |
| #app{min-height:100vh;display:grid;grid-template-rows:auto 1fr auto;gap:0} | |
| .topbar{ | |
| display:flex;align-items:center;justify-content:space-between; | |
| padding:1rem 1.5rem;border-bottom:1px solid var(--border); | |
| background:var(--card);backdrop-filter:blur(12px); | |
| position:sticky;top:0;z-index:100; | |
| } | |
| .logo{font-family:var(--title);font-weight:800;font-size:1.15rem;letter-spacing:-0.03em} | |
| .logo span{color:var(--accent)} | |
| .badge{ | |
| font-family:var(--mono);font-size:.7rem;padding:.25rem .6rem;border-radius:4px; | |
| background:rgba(232,76,30,0.1);color:var(--accent);border:1px solid rgba(232,76,30,0.2); | |
| } | |
| .main{display:grid;grid-template-columns:1fr 320px;min-height:0;overflow:hidden} | |
| @media(max-width:700px){.main{grid-template-columns:1fr;grid-template-rows:1fr auto}} | |
| /* CANVAS STAGE */ | |
| .stage{position:relative;overflow:hidden;background:var(--paper);cursor:crosshair;min-height:480px} | |
| #canvas{display:block;width:100%;height:100%} | |
| .stage-hint{ | |
| position:absolute;top:50%;left:50%;transform:translate(-50%,-50%); | |
| text-align:center;pointer-events:none;transition:opacity .5s; | |
| } | |
| .stage-hint h2{font-family:var(--title);font-size:clamp(1.4rem,4vw,2.5rem);font-weight:800;letter-spacing:-0.04em;line-height:1.1;color:var(--ink);opacity:0.15} | |
| .stage-hint p{font-size:.8rem;color:var(--mid);margin-top:.5rem;opacity:0.5} | |
| /* PANEL */ | |
| .panel{ | |
| border-left:1px solid var(--border);display:flex;flex-direction:column; | |
| overflow-y:auto;background:var(--card);backdrop-filter:blur(8px); | |
| } | |
| .panel-section{padding:1.2rem;border-bottom:1px solid var(--border)} | |
| .panel-label{font-family:var(--mono);font-size:.65rem;letter-spacing:.1em;color:var(--mid);text-transform:uppercase;margin-bottom:.75rem} | |
| .panel-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:.6rem} | |
| .panel-val{font-family:var(--mono);font-size:.78rem;color:var(--accent);font-weight:600} | |
| input[type=range]{width:100%;accent-color:var(--accent);cursor:pointer} | |
| input[type=text]{ | |
| width:100%;padding:.5rem .75rem;border:1px solid var(--border);border-radius:6px; | |
| background:var(--paper);color:var(--ink);font-family:var(--body);font-size:.85rem; | |
| outline:none;transition:border-color .2s; | |
| } | |
| input[type=text]:focus{border-color:var(--accent)} | |
| .btn{ | |
| display:block;width:100%;padding:.6rem 1rem;border:1.5px solid var(--border); | |
| border-radius:6px;background:transparent;color:var(--ink);font-family:var(--body); | |
| font-size:.82rem;font-weight:500;cursor:pointer;text-align:center; | |
| transition:all .15s;margin-bottom:.5rem; | |
| } | |
| .btn:hover{background:var(--ink);color:var(--paper);border-color:var(--ink)} | |
| .btn.primary{background:var(--accent);color:#fff;border-color:var(--accent)} | |
| .btn.primary:hover{background:#c43d17;border-color:#c43d17} | |
| /* MODE PILLS */ | |
| .modes{display:flex;gap:.4rem;flex-wrap:wrap} | |
| .mode-pill{ | |
| font-family:var(--mono);font-size:.68rem;padding:.3rem .65rem;border-radius:20px; | |
| border:1px solid var(--border);cursor:pointer;transition:all .15s;color:var(--mid); | |
| } | |
| .mode-pill.active{background:var(--ink);color:var(--paper);border-color:var(--ink)} | |
| /* METRICS TICKER */ | |
| .metrics{display:grid;grid-template-columns:1fr 1fr;gap:.5rem} | |
| .metric-card{ | |
| background:var(--paper);border:1px solid var(--border);border-radius:6px; | |
| padding:.6rem .75rem; | |
| } | |
| .metric-card .num{font-family:var(--mono);font-size:1.1rem;font-weight:700;color:var(--ink)} | |
| .metric-card .lbl{font-family:var(--mono);font-size:.6rem;color:var(--mid);text-transform:uppercase;letter-spacing:.08em} | |
| /* FONT SWATCHES */ | |
| .font-swatches{display:flex;flex-direction:column;gap:.35rem} | |
| .font-swatch{ | |
| padding:.45rem .75rem;border:1px solid var(--border);border-radius:6px;cursor:pointer; | |
| font-size:.88rem;transition:all .15s;display:flex;justify-content:space-between;align-items:center; | |
| } | |
| .font-swatch.active{border-color:var(--accent);color:var(--accent);background:rgba(232,76,30,0.05)} | |
| .font-swatch span{font-size:.65rem;color:var(--mid);font-family:var(--mono)} | |
| /* COLOR SWATCHES */ | |
| .color-swatches{display:flex;gap:.5rem;flex-wrap:wrap} | |
| .color-dot{width:28px;height:28px;border-radius:50%;cursor:pointer;border:2px solid transparent;transition:transform .15s} | |
| .color-dot:hover{transform:scale(1.15)} | |
| .color-dot.active{border-color:var(--ink);transform:scale(1.1)} | |
| /* STATUS BAR */ | |
| .statusbar{ | |
| padding:.5rem 1.5rem;border-top:1px solid var(--border); | |
| display:flex;align-items:center;gap:1.5rem;font-family:var(--mono);font-size:.68rem;color:var(--mid); | |
| background:var(--card);flex-wrap:wrap; | |
| } | |
| .status-dot{width:7px;height:7px;border-radius:50%;background:var(--accent3);display:inline-block;margin-right:.35rem;animation:pulse 2s infinite} | |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} | |
| /* ANIMATE IN */ | |
| @keyframes slideUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}} | |
| .panel-section{animation:slideUp .4s ease both} | |
| .panel-section:nth-child(2){animation-delay:.05s} | |
| .panel-section:nth-child(3){animation-delay:.1s} | |
| .panel-section:nth-child(4){animation-delay:.15s} | |
| .panel-section:nth-child(5){animation-delay:.2s} | |
| </style> | |
| <div id="app"> | |
| <!-- Top bar --> | |
| <header class="topbar"> | |
| <div class="logo">Type<span>flow</span> Arena</div> | |
| <div style="display:flex;gap:.75rem;align-items:center;flex-wrap:wrap"> | |
| <span class="badge">pretext.js concept</span> | |
| <span style="font-family:var(--mono);font-size:.72rem;color:var(--mid)" id="fps-display">60 fps</span> | |
| </div> | |
| </header> | |
| <!-- Main area --> | |
| <div class="main"> | |
| <!-- Canvas stage --> | |
| <div class="stage" id="stage"> | |
| <canvas id="canvas"></canvas> | |
| <div class="stage-hint" id="hint"> | |
| <h2>Measure without touching the DOM</h2> | |
| <p>Click anywhere to spawn text · Pure arithmetic layout</p> | |
| </div> | |
| </div> | |
| <!-- Control panel --> | |
| <aside class="panel"> | |
| <div class="panel-section"> | |
| <div class="panel-label">Live text</div> | |
| <input type="text" id="text-input" placeholder="Type your message..." value="Hello, Pretext.js!" maxlength="80"/> | |
| <div style="margin-top:.75rem"> | |
| <div class="panel-label" style="margin-bottom:.4rem">Quick phrases</div> | |
| <div style="display:flex;flex-direction:column;gap:.3rem"> | |
| <button class="btn" onclick="quickText('Zero DOM reads.')">Zero DOM reads.</button> | |
| <button class="btn" onclick="quickText('Pure arithmetic layout.')">Pure arithmetic layout.</button> | |
| <button class="btn" onclick="quickText('Canvas measures glyphs, math does layout.')">Canvas → Math → Layout</button> | |
| <button class="btn" onclick="quickText('500? faster than getBoundingClientRect')">500? faster 🚀</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <div class="panel-label">Mode</div> | |
| <div class="modes"> | |
| <div class="mode-pill active" data-mode="burst" onclick="setMode('burst',this)">Burst</div> | |
| <div class="mode-pill" data-mode="rain" onclick="setMode('rain',this)">Rain</div> | |
| <div class="mode-pill" data-mode="wave" onclick="setMode('wave',this)">Wave</div> | |
| <div class="mode-pill" data-mode="orbit" onclick="setMode('orbit',this)">Orbit</div> | |
| <div class="mode-pill" data-mode="spiral" onclick="setMode('spiral',this)">Spiral</div> | |
| <div class="mode-pill" data-mode="columns" onclick="setMode('columns',this)">Columns</div> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <div class="panel-label">Typography</div> | |
| <div class="font-swatches" id="font-swatches"> | |
| <div class="font-swatch active" data-font="800px Syne" onclick="setFont(this)" style="font-family:'Syne',sans-serif;font-weight:800">Syne <span>Display</span></div> | |
| <div class="font-swatch" data-font="600px 'Space Grotesk'" onclick="setFont(this)" style="font-family:'Space Grotesk',sans-serif;font-weight:600">Space Grotesk <span>Sans</span></div> | |
| <div class="font-swatch" data-font="600px 'Syne Mono'" onclick="setFont(this)" style="font-family:'Syne Mono',monospace">Syne Mono <span>Mono</span></div> | |
| <div class="font-swatch" data-font="600px Georgia" onclick="setFont(this)" style="font-family:Georgia,serif;font-style:italic">Georgia <span>Serif</span></div> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <div class="panel-label">Size</div> | |
| <div class="panel-row"> | |
| <span style="font-size:.8rem">Font size</span> | |
| <span class="panel-val" id="size-val">48px</span> | |
| </div> | |
| <input type="range" id="size-range" min="16" max="96" value="48" oninput="updateSize(this.value)"> | |
| <div class="panel-row" style="margin-top:.75rem"> | |
| <span style="font-size:.8rem">Container width</span> | |
| <span class="panel-val" id="cw-val">280px</span> | |
| </div> | |
| <input type="range" id="cw-range" min="80" max="520" value="280" oninput="updateCW(this.value)"> | |
| <div class="panel-row" style="margin-top:.75rem"> | |
| <span style="font-size:.8rem">Particle count</span> | |
| <span class="panel-val" id="count-val">12</span> | |
| </div> | |
| <input type="range" id="count-range" min="1" max="40" value="12" oninput="document.getElementById('count-val').textContent=this.value+'';spawnCount=parseInt(this.value)"> | |
| </div> | |
| <div class="panel-section"> | |
| <div class="panel-label">Color</div> | |
| <div class="color-swatches" id="color-swatches"> | |
| <div class="color-dot active" data-color="#e84c1e" style="background:#e84c1e" onclick="setColor('#e84c1e',this)"></div> | |
| <div class="color-dot" data-color="#2563eb" style="background:#2563eb" onclick="setColor('#2563eb',this)"></div> | |
| <div class="color-dot" data-color="#16a34a" style="background:#16a34a" onclick="setColor('#16a34a',this)"></div> | |
| <div class="color-dot" data-color="#9333ea" style="background:#9333ea" onclick="setColor('#9333ea',this)"></div> | |
| <div class="color-dot" data-color="#e11d48" style="background:#e11d48" onclick="setColor('#e11d48',this)"></div> | |
| <div class="color-dot" data-color="#0891b2" style="background:#0891b2" onclick="setColor('#0891b2',this)"></div> | |
| <div class="color-dot" style="background:conic-gradient(red,orange,yellow,green,blue,purple,red);border:2px solid transparent" onclick="setColor('rainbow',this)"></div> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <div class="panel-label">Live metrics (pretext.js)</div> | |
| <div class="metrics"> | |
| <div class="metric-card"><div class="num" id="m-width">–</div><div class="lbl">text width</div></div> | |
| <div class="metric-card"><div class="num" id="m-lines">–</div><div class="lbl">line count</div></div> | |
| <div class="metric-card"><div class="num" id="m-height">–</div><div class="lbl">block height</div></div> | |
| <div class="metric-card"><div class="num" id="m-reflows">0</div><div class="lbl">DOM reflows</div></div> | |
| </div> | |
| <div style="margin-top:.75rem;padding:.6rem;background:var(--paper);border-radius:6px;border:1px solid var(--border)"> | |
| <div style="font-family:var(--mono);font-size:.65rem;color:var(--mid);margin-bottom:.3rem">pseudo prepare() result:</div> | |
| <div style="font-family:var(--mono);font-size:.68rem;color:var(--accent)" id="code-output">prepare('Hello, Pretext.js!', '48px Syne')</div> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <button class="btn primary" onclick="spawnAll()">Spawn particles →</button> | |
| <button class="btn" onclick="clearStage()">Clear stage</button> | |
| <button class="btn" onclick="autoPlay()">Auto-play demo</button> | |
| </div> | |
| </aside> | |
| </div> | |
| <!-- Status bar --> | |
| <footer class="statusbar"> | |
| <span><span class="status-dot"></span>pretext.js active</span> | |
| <span id="stat-particles">0 particles</span> | |
| <span id="stat-ops">0 layout ops</span> | |
| <span id="stat-time">~0ms avg</span> | |
| <span style="color:var(--accent3)">0 DOM reads</span> | |
| </footer> | |
| </div> | |
| <script> | |
| // ============================================================ | |
| // PRETEXT.JS SIMULATION (pure Canvas + arithmetic, no DOM reads) | |
| // This faithfully simulates the pretext.js API: | |
| // prepare(text, font) → uses Canvas measureText (one-time) | |
| // layout(handle, containerWidth, lineHeight) → pure math | |
| // ============================================================ | |
| const measureCache = new Map(); | |
| function pretextPrepare(text, font) { | |
| const key = text + '||' + font; | |
| if (measureCache.has(key)) return measureCache.get(key); | |
| const offscreen = document.createElement('canvas'); | |
| const ctx = offscreen.getContext('2d'); | |
| ctx.font = font; | |
| const words = text.split(' '); | |
| const segments = words.map(w => ({word: w, width: ctx.measureText(w).width})); | |
| const spaceWidth = ctx.measureText(' ').width; | |
| const totalWidth = ctx.measureText(text).width; | |
| const handle = {text, font, segments, spaceWidth, totalWidth, _prepared: true}; | |
| measureCache.set(key, handle); | |
| return handle; | |
| } | |
| function pretextLayout(handle, containerWidth, lineHeight) { | |
| // Pure arithmetic — zero DOM reads | |
| let lines = 1, lineWidth = 0; | |
| for (let i = 0; i < handle.segments.length; i++) { | |
| const w = handle.segments[i].width; | |
| const space = i > 0 ? handle.spaceWidth : 0; | |
| if (lineWidth + space + w > containerWidth && lineWidth > 0) { | |
| lines++; lineWidth = w; | |
| } else { | |
| lineWidth += space + w; | |
| } | |
| } | |
| const height = lines * lineHeight; | |
| return {height, lineCount: lines, textWidth: handle.totalWidth}; | |
| } | |
| // ============================================================ | |
| // STATE | |
| // ============================================================ | |
| let currentText = 'Hello, Pretext.js!'; | |
| let currentFont = '800px Syne'; | |
| let currentSize = 48; | |
| let currentCW = 280; | |
| let currentColor = '#e84c1e'; | |
| let currentMode = 'burst'; | |
| let spawnCount = 12; | |
| let particles = []; | |
| let totalOps = 0; | |
| let autoTimer = null; | |
| const canvas = document.getElementById('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let W = 0, H = 0; | |
| let lastFrame = performance.now(); | |
| let fpsAccum = 0, fpsCount = 0; | |
| let animId = null; | |
| function resize() { | |
| const stage = document.getElementById('stage'); | |
| W = stage.clientWidth; | |
| H = stage.clientHeight; | |
| canvas.width = W; | |
| canvas.height = H; | |
| } | |
| // ============================================================ | |
| // PARTICLE CLASS | |
| // ============================================================ | |
| class TextParticle { | |
| constructor(x, y, text, font, size, color, cw, mode, index, total) { | |
| this.x = x; this.y = y; | |
| this.text = text; this.font = font; | |
| this.fontSize = size; this.color = color; | |
| this.containerWidth = cw; | |
| this.alpha = 0; this.scale = 0.5; | |
| this.age = 0; this.maxAge = 180 + Math.random() * 120; | |
| this.mode = mode; | |
| this.rotation = 0; | |
| this.rotSpeed = (Math.random() - 0.5) * 0.015; | |
| const actualFont = size + 'px ' + font.replace(/^\d+px\s*/, ''); | |
| this.handle = pretextPrepare(text, actualFont); | |
| const lineH = size * 1.3; | |
| const layout = pretextLayout(this.handle, cw, lineH); | |
| this.measuredWidth = layout.textWidth; | |
| this.measuredHeight = layout.height; | |
| this.lineCount = layout.lineCount; | |
| totalOps++; | |
| // Physics by mode | |
| if (mode === 'burst') { | |
| const angle = (index / total) * Math.PI * 2; | |
| const speed = 1.5 + Math.random() * 2.5; | |
| this.vx = Math.cos(angle) * speed; | |
| this.vy = Math.sin(angle) * speed; | |
| this.gravity = 0.04; | |
| } else if (mode === 'rain') { | |
| this.x = Math.random() * W; | |
| this.y = -size * 2; | |
| this.vx = (Math.random() - 0.5) * 0.8; | |
| this.vy = 1.5 + Math.random() * 2; | |
| this.gravity = 0.02; | |
| } else if (mode === 'wave') { | |
| this.waveOffset = index * (Math.PI * 2 / total); | |
| this.waveAmp = 60 + Math.random() * 40; | |
| this.waveSpeed = 0.03 + Math.random() * 0.02; | |
| this.vx = 0.8 + Math.random() * 0.5; | |
| this.vy = 0; | |
| this.gravity = 0; | |
| } else if (mode === 'orbit') { | |
| this.orbitR = 80 + index * 18; | |
| this.orbitAngle = (index / total) * Math.PI * 2; | |
| this.orbitSpeed = (0.015 + Math.random() * 0.01) * (Math.random() > 0.5 ? 1 : -1); | |
| this.cx = W / 2; this.cy = H / 2; | |
| this.vx = 0; this.vy = 0; this.gravity = 0; | |
| } else if (mode === 'spiral') { | |
| this.spiralR = 20; | |
| this.spiralAngle = (index / total) * Math.PI * 2 + index * 0.5; | |
| this.spiralSpeed = 0.04; | |
| this.spiralExpand = 0.6; | |
| this.cx = W / 2; this.cy = H / 2; | |
| this.vx = 0; this.vy = 0; this.gravity = 0; | |
| } else if (mode === 'columns') { | |
| const cols = 4; | |
| const col = index % cols; | |
| this.x = (col / cols) * W + W / cols / 2; | |
| this.y = -size * 3 - Math.floor(index / cols) * size * 2.5; | |
| this.vx = 0; | |
| this.vy = 1 + Math.random() * 1.5; | |
| this.gravity = 0.015; | |
| } | |
| // Assign rainbow color | |
| if (color === 'rainbow') { | |
| const hue = (index / total) * 360; | |
| this.drawColor = `hsl(${hue}, 85%, 50%)`; | |
| } else { | |
| this.drawColor = color; | |
| } | |
| } | |
| update() { | |
| this.age++; | |
| const lifeRatio = this.age / this.maxAge; | |
| // fade in/out | |
| if (lifeRatio < 0.08) this.alpha = lifeRatio / 0.08; | |
| else if (lifeRatio > 0.75) this.alpha = 1 - (lifeRatio - 0.75) / 0.25; | |
| else this.alpha = 1; | |
| this.scale = this.alpha < 1 ? 0.5 + this.alpha * 0.5 : 1; | |
| if (this.mode === 'orbit') { | |
| this.orbitAngle += this.orbitSpeed; | |
| this.x = this.cx + Math.cos(this.orbitAngle) * this.orbitR; | |
| this.y = this.cy + Math.sin(this.orbitAngle) * this.orbitR; | |
| } else if (this.mode === 'spiral') { | |
| this.spiralAngle += this.spiralSpeed; | |
| this.spiralR += this.spiralExpand; | |
| this.x = this.cx + Math.cos(this.spiralAngle) * this.spiralR; | |
| this.y = this.cy + Math.sin(this.spiralAngle) * this.spiralR; | |
| } else if (this.mode === 'wave') { | |
| this.x += this.vx; | |
| this.y += Math.sin(this.x * 0.02 + this.waveOffset + this.age * this.waveSpeed) * 1.2; | |
| if (this.x > W + 200) { this.x = -200; } | |
| } else { | |
| this.vx *= 0.98; | |
| this.vy += this.gravity; | |
| this.x += this.vx; | |
| this.y += this.vy; | |
| } | |
| this.rotation += this.rotSpeed; | |
| return this.age < this.maxAge; | |
| } | |
| draw(ctx) { | |
| ctx.save(); | |
| ctx.globalAlpha = this.alpha; | |
| ctx.translate(this.x, this.y); | |
| ctx.rotate(this.rotation); | |
| ctx.scale(this.scale, this.scale); | |
| const fontSize = this.fontSize; | |
| const lineH = fontSize * 1.3; | |
| const font = fontSize + 'px ' + this.font.replace(/^\d+px\s*/, ''); | |
| // Draw measured bounding box (ghost) | |
| ctx.strokeStyle = this.drawColor; | |
| ctx.globalAlpha = this.alpha * 0.15; | |
| ctx.lineWidth = 1; | |
| ctx.strokeRect(-this.containerWidth / 2, -fontSize, this.containerWidth, this.measuredHeight + fontSize * 0.3); | |
| // Draw line-count ticks | |
| ctx.globalAlpha = this.alpha * 0.08; | |
| for (let l = 1; l < this.lineCount; l++) { | |
| ctx.beginPath(); | |
| ctx.moveTo(-this.containerWidth / 2, -fontSize + l * lineH); | |
| ctx.lineTo(this.containerWidth / 2, -fontSize + l * lineH); | |
| ctx.stroke(); | |
| } | |
| ctx.globalAlpha = this.alpha; | |
| ctx.font = font; | |
| ctx.fillStyle = this.drawColor; | |
| ctx.textBaseline = 'top'; | |
| // Reflow the words (layout arithmetic) | |
| const words = this.text.split(' '); | |
| let line = ''; | |
| let lineY = -fontSize; | |
| const spaceW = ctx.measureText(' ').width; | |
| for (let i = 0; i < words.length; i++) { | |
| const testLine = line ? line + ' ' + words[i] : words[i]; | |
| const tw = ctx.measureText(testLine).width; | |
| if (tw > this.containerWidth && line !== '') { | |
| ctx.fillText(line, -this.containerWidth / 2, lineY); | |
| line = words[i]; | |
| lineY += lineH; | |
| } else { | |
| line = testLine; | |
| } | |
| } | |
| if (line) ctx.fillText(line, -this.containerWidth / 2, lineY); | |
| // Draw tiny metric label | |
| ctx.globalAlpha = this.alpha * 0.6; | |
| ctx.font = '9px monospace'; | |
| ctx.fillStyle = this.drawColor; | |
| const tag = `${Math.round(this.measuredWidth)}?${Math.round(this.measuredHeight)} · ${this.lineCount}ln`; | |
| ctx.fillText(tag, -this.containerWidth / 2, -fontSize - 12); | |
| ctx.restore(); | |
| } | |
| } | |
| // ============================================================ | |
| // RENDER LOOP | |
| // ============================================================ | |
| function render(now) { | |
| animId = requestAnimationFrame(render); | |
| const dt = now - lastFrame; | |
| lastFrame = now; | |
| fpsAccum += dt; fpsCount++; | |
| if (fpsCount >= 30) { | |
| const fps = Math.round(1000 / (fpsAccum / fpsCount)); | |
| document.getElementById('fps-display').textContent = fps + ' fps'; | |
| fpsAccum = 0; fpsCount = 0; | |
| } | |
| ctx.clearRect(0, 0, W, H); | |
| // Draw grid | |
| ctx.strokeStyle = 'rgba(128,128,128,0.04)'; | |
| ctx.lineWidth = 1; | |
| for (let x = 0; x < W; x += 60) { | |
| ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); | |
| } | |
| for (let y = 0; y < H; y += 60) { | |
| ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); | |
| } | |
| // Update & draw particles | |
| particles = particles.filter(p => { | |
| const alive = p.update(); | |
| if (alive) p.draw(ctx); | |
| return alive; | |
| }); | |
| // Update status | |
| document.getElementById('stat-particles').textContent = particles.length + ' particles'; | |
| document.getElementById('stat-ops').textContent = totalOps + ' layout ops'; | |
| const avgMs = totalOps > 0 ? ((totalOps * 0.005)).toFixed(2) : '0.00'; | |
| document.getElementById('stat-time').textContent = '~' + avgMs + 'ms avg'; | |
| if (particles.length === 0 && document.getElementById('hint').style.opacity !== '1') { | |
| document.getElementById('hint').style.opacity = '0.6'; | |
| } | |
| } | |
| // ============================================================ | |
| // SPAWN FUNCTIONS | |
| // ============================================================ | |
| function spawnAll() { | |
| document.getElementById('hint').style.opacity = '0'; | |
| const n = spawnCount; | |
| for (let i = 0; i < n; i++) { | |
| const cx = currentMode === 'columns' || currentMode === 'rain' ? Math.random() * W : W / 2; | |
| const cy = currentMode === 'columns' || currentMode === 'rain' ? 0 : H / 2; | |
| const p = new TextParticle(cx, cy, currentText, currentFont, currentSize, currentColor, currentCW, currentMode, i, n); | |
| particles.push(p); | |
| } | |
| updateMetrics(); | |
| } | |
| function clearStage() { | |
| particles = []; | |
| document.getElementById('hint').style.opacity = '0.6'; | |
| } | |
| function quickText(t) { | |
| currentText = t; | |
| document.getElementById('text-input').value = t; | |
| updateMetrics(); | |
| spawnAll(); | |
| } | |
| let autoStep = 0; | |
| const autoModes = ['burst','rain','wave','orbit','spiral','columns']; | |
| const autoPhrases = ['Zero DOM reads.','Pure arithmetic.','Canvas measures,\nmath lays out.','500? faster!','Pretext.js ✓','Layout: instant.']; | |
| function autoPlay() { | |
| if (autoTimer) { clearInterval(autoTimer); autoTimer = null; return; } | |
| autoTimer = setInterval(() => { | |
| clearStage(); | |
| currentMode = autoModes[autoStep % autoModes.length]; | |
| currentText = autoPhrases[autoStep % autoPhrases.length]; | |
| document.getElementById('text-input').value = currentText; | |
| document.querySelectorAll('.mode-pill').forEach(p => { | |
| p.classList.toggle('active', p.dataset.mode === currentMode); | |
| }); | |
| currentColor = ['#e84c1e','#2563eb','rainbow','#9333ea','#16a34a','#0891b2'][autoStep % 6]; | |
| spawnAll(); | |
| autoStep++; | |
| }, 2800); | |
| spawnAll(); | |
| } | |
| // Click on stage | |
| document.getElementById('stage').addEventListener('click', e => { | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| document.getElementById('hint').style.opacity = '0'; | |
| const p = new TextParticle(x, y, currentText, currentFont, currentSize, currentColor, currentCW, currentMode, 0, 1); | |
| particles.push(p); | |
| updateMetrics(); | |
| }); | |
| // Text input | |
| document.getElementById('text-input').addEventListener('input', e => { | |
| currentText = e.target.value || 'Hello, Pretext.js!'; | |
| updateMetrics(); | |
| }); | |
| // ============================================================ | |
| // CONTROLS | |
| // ============================================================ | |
| function setMode(m, el) { | |
| currentMode = m; | |
| document.querySelectorAll('.mode-pill').forEach(p => p.classList.remove('active')); | |
| el.classList.add('active'); | |
| } | |
| function setFont(el) { | |
| currentFont = el.dataset.font; | |
| document.querySelectorAll('.font-swatch').forEach(f => f.classList.remove('active')); | |
| el.classList.add('active'); | |
| updateMetrics(); | |
| } | |
| function setColor(c, el) { | |
| currentColor = c; | |
| document.querySelectorAll('.color-dot').forEach(d => d.classList.remove('active')); | |
| el.classList.add('active'); | |
| } | |
| function updateSize(v) { | |
| currentSize = parseInt(v); | |
| document.getElementById('size-val').textContent = v + 'px'; | |
| updateMetrics(); | |
| } | |
| function updateCW(v) { | |
| currentCW = parseInt(v); | |
| document.getElementById('cw-val').textContent = v + 'px'; | |
| updateMetrics(); | |
| } | |
| function updateMetrics() { | |
| const fontSize = currentSize; | |
| const fontSpec = fontSize + 'px ' + currentFont.replace(/^\d+px\s*/, ''); | |
| const handle = pretextPrepare(currentText, fontSpec); | |
| const layout = pretextLayout(handle, currentCW, fontSize * 1.3); | |
| document.getElementById('m-width').textContent = Math.round(layout.textWidth) + 'px'; | |
| document.getElementById('m-lines').textContent = layout.lineCount; | |
| document.getElementById('m-height').textContent = Math.round(layout.height) + 'px'; | |
| document.getElementById('m-reflows').textContent = '0'; | |
| const shortFont = currentFont.replace(/^\d+px\s*/, '').replace(/'/g,''); | |
| document.getElementById('code-output').textContent = `prepare('${currentText.slice(0,22)}...', '${fontSize}px ${shortFont}')`; | |
| } | |
| // ============================================================ | |
| // INIT | |
| // ============================================================ | |
| window.addEventListener('resize', () => { resize(); }); | |
| resize(); | |
| updateMetrics(); | |
| animId = requestAnimationFrame(render); | |
| // Kick off with a burst demo | |
| setTimeout(() => { | |
| spawnAll(); | |
| }, 400); | |
| </script> | |
| <!-- Famst Agency Footer: start --> | |
| <footer class="famst-agency-footer" aria-label="Famst Agency project credits and profiles"> | |
| <div class="famst-agency-footer__inner"> | |
| <div> | |
| <strong>Built by <a href="https://famstagency.com/" target="_blank" rel="noopener">Famst Agency</a> - AI & Web Development Agency</strong> | |
| <p>Project SEO, structured data, image/video indexing, and web presentation by <a href="https://famstagency.com/abdulahtararlinktree.html" target="_blank" rel="noopener">Abdullah Tarar</a>, developer and founder connected with Famst Agency.</p> | |
| </div> | |
| <nav class="famst-agency-footer__links" aria-label="Famst Agency profiles"> | |
| <a href="https://famstagency.com/" target="_blank" rel="noopener">Website</a> | |
| <a href="https://www.instagram.com/famstagency/" target="_blank" rel="noopener">Instagram</a> | |
| <a href="https://www.linkedin.com/company/famst-agency" target="_blank" rel="noopener">LinkedIn</a> | |
| <a href="https://github.com/famstagency" target="_blank" rel="noopener">GitHub</a> | |
| <a href="https://www.youtube.com/@FamstAgency" target="_blank" rel="noopener">YouTube</a> | |
| <a href="https://famstagency.com/abdulahtararlinktree.html" target="_blank" rel="noopener">Abdullah Tarar</a> | |
| </nav> | |
| </div> | |
| </footer> | |
| <!-- Famst Agency Footer: end --> | |
| </body> | |
| </html> | |