أنشئ تأثيرات نصية مذهلة مع Pretext و Next.js — من تخطيطات المجلات إلى الفن التفاعلي

Noqta Team
بواسطة Noqta Team ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

مطوّر من Midjourney اخترق "أعماق الجحيم" ليمنحنا Pretext — مكتبة بحجم 15 كيلوبايت تقيس النصوص وتنسّقها بسرعة تفوق المتصفح بـ 500 مرة، دون أن تلمس الـ DOM.

المطوّرون بدأوا يُبدعون بها بالفعل. أحدهم بنى لعبة Text Invaders. وآخرون ينشئون تخطيطات بجودة المجلات، وطباعة حركية، ونصوصاً تتدفق حول الأشكال.

في هذا الدرس، ستبني 4 تأثيرات نصية إبداعية باستخدام Pretext و Next.js — والأخيرة منها تُعرض هذا المقال نفسه بطباعة بجودة مجلة راقية. ألا تبدو فكرة مثيرة؟

المتطلبات الأساسية

  • Node.js 18+
  • Next.js 14+ (App Router)
  • معرفة أساسية بـ React/TypeScript
  • إحساس بالطموح الجمالي

الإعداد

npx create-next-app@latest pretext-creative --typescript --tailwind --app
cd pretext-creative
npm install pretext

التأثير الأول: نص يتدفق حول الصور (تخطيط المجلة)

الحيلة الكلاسيكية التي فشل CSS في تحقيقها لـ 30 عاماً. مع Pretext، يلتفّ النص حول أشكال عشوائية — ليس فقط المستطيلات.

// app/magazine/page.tsx
'use client';
 
import { useEffect, useRef } from 'react';
import { prepare, layoutWithLines } from 'pretext';
 
export default function MagazinePage() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
 
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d')!;
 
    canvas.width = 800;
    canvas.height = 600;
 
    const article = `The web has treated text as a second-class citizen for three decades. While print designers flowed paragraphs around images and wrapped columns with ease, browsers offered none of that without expensive DOM reflows. Until now. Pretext changes everything about how we think about text on the web. Magazine layouts, kinetic typography, and text art — all at 120fps.`;
 
    // Prepare text measurements (cached, ~19ms for 500 paragraphs)
    const prepared = prepare(ctx, article, {
      fontFamily: 'Georgia',
      fontSize: 18,
      lineHeight: 28,
    });
 
    // Define an obstacle (circular image area)
    const obstacle = { x: 500, y: 50, radius: 120 };
 
    // Layout with variable widths per line to flow around obstacle
    let y = 40;
    const lines = layoutWithLines(prepared, 720);
 
    // Draw obstacle
    ctx.beginPath();
    ctx.arc(obstacle.x, obstacle.y + 80, obstacle.radius, 0, Math.PI * 2);
    ctx.fillStyle = '#f0f0f0';
    ctx.fill();
    ctx.fillStyle = '#999';
    ctx.font = '14px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('Your image here', obstacle.x, obstacle.y + 85);
 
    // Draw text flowing around obstacle
    ctx.fillStyle = '#1a1a1a';
    ctx.font = '18px Georgia';
    ctx.textAlign = 'left';
 
    lines.forEach((line) => {
      const lineCenter = y + 14;
      const dy = lineCenter - (obstacle.y + 80);
      let xStart = 40;
      let maxWidth = 720;
 
      // If line overlaps with obstacle, indent
      if (Math.abs(dy) < obstacle.radius) {
        const indent = Math.sqrt(obstacle.radius ** 2 - dy ** 2);
        maxWidth = obstacle.x - indent - 60;
      }
 
      ctx.fillText(line.text.trim(), xStart, y + 20);
      y += 28;
    });
 
  }, []);
 
  return (
    <main className="min-h-screen bg-white flex items-center justify-center p-8">
      <div>
        <h1 className="text-3xl font-bold mb-6">Magazine Text Flow</h1>
        <canvas ref={canvasRef} className="border border-gray-200 rounded-lg shadow-sm" />
        <p className="text-sm text-gray-500 mt-4">
          Text flows around the circular obstacle — no DOM reflows, no CSS hacks.
        </p>
      </div>
    </main>
  );
}

ما الذي يحدث هنا: prepare() تخزّن جميع قياسات النص باستخدام Canvas (وليس الـ DOM). ثم نحسب عرض كل سطر بناءً على شكل العائق. يلتفّ النص بشكل طبيعي حول الدائرة — بمعدل 120 إطاراً في الثانية.

التأثير الثاني: الطباعة الحركية (أنيميشن ظهور النص)

النوع من التأثيرات الذي تُقدّم وكالات التصميم الإبداعي فاتورة بـ 5000 دولار عليه. كلمات تنزلق داخل الشاشة بتوقيت متدرج وأحجام متغيرة.

// app/kinetic/page.tsx
'use client';
 
import { useEffect, useRef, useState } from 'react';
import { prepare, layout } from 'pretext';
 
const words = [
  { text: 'AI AGENTS', size: 72, weight: 'bold', color: '#0070F4' },
  { text: 'SHIP CODE', size: 64, weight: 'bold', color: '#1a1a1a' },
  { text: 'RUN AUDITS', size: 56, weight: 'normal', color: '#3ABAB4' },
  { text: 'MANAGE PROJECTS', size: 48, weight: 'normal', color: '#666' },
  { text: '$45/HR', size: 80, weight: 'bold', color: '#0070F4' },
  { text: 'HUMAN IN THE LOOP', size: 40, weight: 'normal', color: '#999' },
];
 
export default function KineticPage() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [frame, setFrame] = useState(0);
 
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d')!;
    canvas.width = 900;
    canvas.height = 600;
 
    let animFrame: number;
    let startTime = Date.now();
 
    const animate = () => {
      const elapsed = (Date.now() - startTime) / 1000;
      ctx.clearRect(0, 0, 900, 600);
      ctx.fillStyle = '#fafafa';
      ctx.fillRect(0, 0, 900, 600);
 
      let y = 60;
      words.forEach((word, i) => {
        const delay = i * 0.3;
        const progress = Math.min(1, Math.max(0, (elapsed - delay) / 0.5));
        const eased = 1 - Math.pow(1 - progress, 3); // ease-out cubic
 
        const alpha = eased;
        const offsetX = (1 - eased) * 100;
 
        // Use Pretext to measure exact width for centering
        ctx.font = `${word.weight} ${word.size}px "Inter", sans-serif`;
        const prepared = prepare(ctx, word.text, {
          fontFamily: '"Inter", sans-serif',
          fontSize: word.size,
          fontWeight: word.weight,
        });
        const height = layout(prepared, 900);
 
        ctx.globalAlpha = alpha;
        ctx.fillStyle = word.color;
        ctx.fillText(word.text, 50 + offsetX, y + word.size * 0.8);
        ctx.globalAlpha = 1;
 
        y += word.size + 16;
      });
 
      animFrame = requestAnimationFrame(animate);
    };
 
    animate();
    return () => cancelAnimationFrame(animFrame);
  }, []);
 
  return (
    <main className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
      <div>
        <h1 className="text-2xl font-bold mb-4">Kinetic Typography</h1>
        <canvas ref={canvasRef} className="rounded-xl shadow-lg" />
        <p className="text-sm text-gray-500 mt-4">
          Each line slides in with staggered timing. Pretext measures text instantly for perfect positioning.
        </p>
      </div>
    </main>
  );
}

لماذا Pretext مهمة هنا: بدون Pretext، يتطلب حساب عرض النص للمحاذاة أو التوسيط إدراج عناصر في الـ DOM ثم انتظار إعادة الرسم. في أنيميشن 60 إطاراً في الثانية، هذا كارثي. Pretext تفعل ذلك في 0.09 ميلي ثانية.

التأثير الثالث: نص بشكل كائن (الشعر البصري)

اجعل النص يملأ أي شكل عشوائي — دائرة، قلب، مخطط شعار. النوع من الأشياء الذي يصبح فيروسياً على تويتر.

// app/shape-text/page.tsx
'use client';
 
import { useEffect, useRef } from 'react';
import { prepare, layoutNextLine } from 'pretext';
 
export default function ShapeTextPage() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
 
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d')!;
    canvas.width = 600;
    canvas.height = 600;
 
    const text = `Pretext measures and lays out multiline text without triggering DOM reflows. It uses the Canvas measureText API to build its own measurement cache. From that cache, it calculates paragraph heights, returns individual line objects, and lays out text line by line with variable widths. This makes text flow around obstacles, wrap into columns, and fit into arbitrary shapes — capabilities that were impossible on the web before. Built by Cheng Lou, a Midjourney engineer and former React core team member, Pretext delivers magazine quality typography at 120 frames per second. The library is just 15 kilobytes and works with DOM, Canvas, and SVG.`;
 
    const prepared = prepare(ctx, text, {
      fontFamily: 'Georgia',
      fontSize: 14,
      lineHeight: 20,
    });
 
    // Circle shape: calculate width per line based on circle equation
    const centerX = 300;
    const centerY = 300;
    const radius = 250;
    const lineHeight = 20;
 
    ctx.fillStyle = '#1a1a1a';
    ctx.font = '14px Georgia';
 
    let y = centerY - radius + 20;
    let remaining = text;
 
    while (y < centerY + radius - 20 && remaining.length > 0) {
      const dy = y - centerY;
      const halfWidth = Math.sqrt(Math.max(0, radius * radius - dy * dy));
      const lineWidth = halfWidth * 2 - 40; // padding
 
      if (lineWidth > 50) {
        const x = centerX - halfWidth + 20;
        // Use layoutNextLine to get exactly one line at this width
        const linePrepared = prepare(ctx, remaining, {
          fontFamily: 'Georgia',
          fontSize: 14,
          lineHeight: 20,
        });
 
        // Approximate: measure how many chars fit in lineWidth
        let charCount = 0;
        let measuredWidth = 0;
        for (let i = 0; i < remaining.length; i++) {
          const w = ctx.measureText(remaining.substring(0, i + 1)).width;
          if (w > lineWidth) break;
          charCount = i + 1;
          // Break at word boundary
          if (remaining[i] === ' ') measuredWidth = i + 1;
        }
        const breakAt = measuredWidth || charCount;
        const lineText = remaining.substring(0, breakAt).trim();
        remaining = remaining.substring(breakAt);
 
        ctx.fillText(lineText, x, y);
      }
      y += lineHeight;
    }
 
    // Draw subtle circle outline
    ctx.strokeStyle = '#e0e0e0';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
    ctx.stroke();
 
  }, []);
 
  return (
    <main className="min-h-screen bg-white flex items-center justify-center p-8">
      <div className="text-center">
        <h1 className="text-2xl font-bold mb-4">Text in a Circle</h1>
        <canvas ref={canvasRef} className="mx-auto" />
        <p className="text-sm text-gray-500 mt-4">
          Text fills a circular shape with per-line width calculations. Pure Canvas, zero DOM.
        </p>
      </div>
    </main>
  );
}

الزاوية التي تجذب التفاعل على تويتر: هذا هو النوع من المرئيات الذي يحصد 10 آلاف إعجاب. "جعلت النص يملأ دائرة باستخدام 15 كيلوبايت من JavaScript. لا CSS. لا DOM. مجرد رياضيات."

التأثير الرابع: مقال يعرض نفسه (هذا المقال بأسلوب مجلة)

الخاتمة المدهشة: مكوّن يأخذ محتوى هذا الدرس بصيغة markdown ويعرضه بطباعة راقية بأسلوب مجلة — مع حروف أولية كبيرة، وأعمدة متدفقة، واقتباسات بارزة.

// app/article-renderer/page.tsx
'use client';
 
import { useEffect, useRef } from 'react';
import { prepare, layoutWithLines } from 'pretext';
 
const articleContent = {
  title: 'The 15KB Library That Changed Web Typography',
  subtitle: 'How Pretext makes magazine layouts possible at 120fps',
  body: `For 30 years, the web has treated text as a second-class citizen. While print designers flowed paragraphs around images, wrapped columns, and fit type into arbitrary shapes, browsers offered none of that without expensive DOM reflows.
 
Pretext changes everything. A 15KB TypeScript library by Midjourney engineer Cheng Lou, it measures and lays out multiline text up to 500 times faster than traditional browser methods.
 
The secret is simple: instead of inserting text into the DOM and measuring the result, Pretext uses the Canvas measureText API to build its own measurement cache. From that cache, it calculates heights, returns line objects, and flows text around obstacles.
 
Developers are already building text games, magazine layouts, and kinetic typography with it. The library handles every script including Arabic, emoji, and bidirectional text.
 
This is not an incremental improvement. This is the missing piece of web typography.`,
  pullQuote: '"I crawled through depths of hell to bring you this." — Cheng Lou',
};
 
export default function ArticleRenderer() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
 
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d')!;
    canvas.width = 900;
    canvas.height = 1000;
 
    // Background
    ctx.fillStyle = '#FFFDF7';
    ctx.fillRect(0, 0, 900, 1000);
 
    // Title
    ctx.fillStyle = '#1a1a1a';
    ctx.font = 'bold 36px Georgia';
    ctx.fillText(articleContent.title, 60, 80);
 
    // Subtitle
    ctx.fillStyle = '#666';
    ctx.font = 'italic 18px Georgia';
    ctx.fillText(articleContent.subtitle, 60, 115);
 
    // Divider
    ctx.strokeStyle = '#0070F4';
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(60, 135);
    ctx.lineTo(200, 135);
    ctx.stroke();
 
    // Body text with drop cap
    const paragraphs = articleContent.body.split('\n\n');
    let y = 170;
 
    paragraphs.forEach((para, pIdx) => {
      const prepared = prepare(ctx, para, {
        fontFamily: 'Georgia',
        fontSize: 16,
        lineHeight: 26,
      });
 
      const lines = layoutWithLines(prepared, pIdx === 0 ? 680 : 780);
 
      if (pIdx === 0 && lines.length > 0) {
        // Drop cap for first paragraph
        const firstChar = para[0];
        ctx.fillStyle = '#0070F4';
        ctx.font = 'bold 72px Georgia';
        ctx.fillText(firstChar, 60, y + 60);
 
        // Indent first 3 lines
        ctx.fillStyle = '#1a1a1a';
        ctx.font = '16px Georgia';
        const dropCapWidth = 70;
 
        lines.forEach((line, lIdx) => {
          const x = lIdx < 3 ? 60 + dropCapWidth : 60;
          ctx.fillText(line.text.trim(), x, y);
          y += 26;
        });
      } else {
        ctx.fillStyle = '#1a1a1a';
        ctx.font = '16px Georgia';
        lines.forEach((line) => {
          ctx.fillText(line.text.trim(), 60, y);
          y += 26;
        });
      }
 
      y += 16; // paragraph spacing
 
      // Insert pull quote after second paragraph
      if (pIdx === 1) {
        ctx.fillStyle = '#0070F4';
        ctx.fillRect(60, y, 4, 60);
        ctx.fillStyle = '#333';
        ctx.font = 'italic 20px Georgia';
        ctx.fillText(articleContent.pullQuote, 80, y + 30);
        y += 90;
      }
    });
 
    // Footer
    ctx.fillStyle = '#ccc';
    ctx.font = '12px sans-serif';
    ctx.fillText('Rendered with Pretext — 0 DOM reflows', 60, y + 30);
 
  }, []);
 
  return (
    <main className="min-h-screen bg-gray-100 flex items-center justify-center p-8">
      <div className="text-center">
        <h1 className="text-2xl font-bold mb-4">Self-Rendering Magazine Article</h1>
        <canvas ref={canvasRef} className="rounded-xl shadow-xl mx-auto" />
        <p className="text-sm text-gray-500 mt-4">
          This article about Pretext is rendered BY Pretext. Drop caps, pull quotes, flowing columns — zero DOM.
        </p>
      </div>
    </main>
  );
}

ما الذي بنيته

التأثيرالتقنية المستخدمةالقوة الفيروسية
تدفق أسلوب المجلةنص حول العوائق"CSS لم تستطع يوماً هذا"
الطباعة الحركيةنص متحرك بـ 120fpsجودة وكالة، بدون ميزانية
الشعر البصرينص يملأ الأشكال"صُنع بـ 15 كيلوبايت من JS"
مقال يعرض نفسهتخطيط مجلة ميتا"هذا المقال يعرض نفسه"

لماذا تغيّر Pretext قواعد اللعبة

قبل Pretext، كان كل تأثير نصي إبداعي يستلزم إما:

  • التلاعب بالـ DOM (بطيء، يُفعّل إعادة الرسم، يتعثّر على نطاق واسع)
  • صور مُسبقة التوليد (لا يمكن فهرستها، لا يمكن الوصول إليها، غير متجاوبة)
  • WebGL (تكلفة باهظة لمجرد نص)

Pretext تمنحك طباعة بجودة الطباعة الورقية بسرعة الويب الأصيلة. تتعامل المكتبة مع العربية، والإيموجي، والنصوص ثنائية الاتجاه، وكل الخطوط — بـ 15 كيلوبايت فقط.

الويب أخيراً حصل على محرك نصوص يُعامل الكلمات كمادة إبداعية من الدرجة الأولى.

🚀 هل تبني تطبيقاً غنياً بالمحتوى أو أداة إبداعية؟ وكلاؤنا يُشحنون Next.js إنتاجياً مع طباعة متقدمة وأنيميشن وتحسين للأداء — بـ 45 دولاراً في الساعة، مع إشراف بشري. احجز مكالمة مجانية ←

ماذا تبني بعد ذلك

  • موقع محفظة أعمال بتخطيطات مقالات بجودة المجلات
  • مولّد بطاقات وسائل التواصل الاجتماعي بنصوص على أشكال (مثل Canva لكن أسرع)
  • قصة تفاعلية يُعيد فيها النص تدفّقه مع تمرير القارئ
  • مدونة مباشرة تعرض المقالات بحروف أولية كبيرة واقتباسات بارزة في الوقت الفعلي

مجتمع البرمجة الإبداعية على تويتر يدفع حدود Pretext بالفعل. أحدهم بنى Text Invaders. ماذا ستبني أنت؟

💡 تحتاج مساعدة في بناء منتج ويب إبداعي؟ من محركات الطباعة إلى التجارب التفاعلية، وكلاؤنا الذكيون يتكفّلون بالكود بينما تركّز أنت على الرؤية. تحدّث مع وكيل ←

مزيد من القراءة


الويب كان يمتلك الصور والفيديو والثلاثي الأبعاد لسنوات. النص وحده ظلّ عالقاً في التسعينيات. لم يعد هذا ينطبق اليوم.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على بناء خدمات مصغرة Node.js مع Docker و RabbitMQ و API Gateway.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة