React 实现爱心花园动画

主页:

javascript 复制代码
import React, { useEffect, useRef, useState } from 'react';
import '@/assets/css/Love.less';
import { Garden } from '@/utils/GardenClasses';

// 组件属性接口
interface LoveAnimationProps {
    startDate?: Date; // 可选的开始日期
    messages?: {      // 可自定义的文本消息
        initial?: string;   // 初始文字
        love?: string;      // 告白文字
        signature?: string; // 落款
    };
}

// 默认开始日期:2010年11月2日20点
const DEFAULT_START_DATE = new Date(2010, 10, 2, 20, 0, 0);

// 默认文本配置
const DEFAULT_MESSAGES = {
    initial: "亲爱的,这是我们相爱在一起的时光。",
    love: "爱你直到永永远远。",
    signature: "-爱你的人"
};

const LoveAnimation: React.FC<LoveAnimationProps> = ({
    startDate = DEFAULT_START_DATE,
    messages = DEFAULT_MESSAGES
}) => {
    // ========== Refs定义 ==========
    const canvasRef = useRef<HTMLCanvasElement>(null);      // 画布引用
    const gardenRef = useRef<Garden | null>(null);          // 花园实例引用
    const loveHeartRef = useRef<HTMLDivElement>(null);      // 心形容器
    const contentRef = useRef<HTMLDivElement>(null);        // 内容容器
    const codeRef = useRef<HTMLDivElement>(null);           // 代码区域
    const wordsRef = useRef<HTMLDivElement>(null);          // 文字区域
    const messagesRef = useRef<HTMLDivElement>(null);       // 消息区域
    const loveURef = useRef<HTMLDivElement>(null);          // 告白区域
    const elapseClockRef = useRef<HTMLDivElement>(null);    // 计时器
    const errorMsgRef = useRef<HTMLDivElement>(null);       // 错误信息

    // ========== 状态定义 ==========
    const [showMessages, setShowMessages] = useState(false); // 是否显示消息
    const [showLoveU, setShowLoveU] = useState(false);      // 是否显示告白
    const [codeContent, setCodeContent] = useState('');     // 代码内容
    const [showCursor, setShowCursor] = useState(false);    // 是否显示光标
    const [clearVal, setClearVal] = useState(true);         // 清除标志
    const clearValRef = useRef(clearVal);                   // 清除标志的ref

    // 动画定时器存储
    const animationRefs = useRef<{ 
        intervals: NodeJS.Timeout[]; // 间隔定时器
        timeouts: NodeJS.Timeout[];  // 延时定时器
    }>({ intervals: [], timeouts: [] });

    // 完整的代码内容(带HTML格式)
    const fullCodeContent = `<br />/**
                            <br />*2013---02-14,
                            <br />*2013-02-28.
                            <br />*/
                            <br />Boy name = <span class="keyword">Mr</span> ***
                            <br />Girl name = <span class="keyword">Mrs</span> ***
                            <br /><span class="comments">// Fall in love river.</span>
                            <br />The boy love the girl;
                            <br /><span class="comments">// They love each other.</span>
                            <br />The girl loved the boy;
                            <br /><span class="comments">// AS time goes on.</span>
                            <br />The boy can not be separated the girl;
                            <br /><span class="comments">// At the same time.</span>
                            <br />The girl can not be separated the boy;
                            <br /><span class="comments">// Both wind and snow all over the sky.</span>
                            <br /><span class="comments">// Whether on foot or 5 kilometers.</span>
                            <br /><span class="keyword">The boy</span> very <span class="keyword">happy</span>;
                            <br /><span class="keyword">The girl</span> is also very <span class="keyword">happy</span>;
                            <br /><span class="comments">// Whether it is right now</span>
                            <br /><span class="comments">// Still in the distant future.</span>
                            <br />The boy has but one dream;
                            <br /><span class="comments">// The boy wants the girl could well have been happy.</span>

                            <br />I want to say:
                            <br />Baby, I love you forever;`;

    // ========== 主要副作用 ==========
    useEffect(() => {
        if (!canvasRef.current || !loveHeartRef.current || !contentRef.current) return;

        // 检查浏览器是否支持canvas
        if (!document.createElement('canvas').getContext) {
            if (errorMsgRef.current) {
                errorMsgRef.current.innerHTML =
                    "您的浏览器不支持HTML5!<br/>推荐使用 Chrome 14+/IE 9+/Firefox 7+/Safari 4+";
            }
            if (codeRef.current) {
                codeRef.current.style.display = "none";
            }
            return;
        }

        // 初始化画布
        const gardenCanvas = canvasRef.current;
        gardenCanvas.width = loveHeartRef.current.offsetWidth;
        gardenCanvas.height = loveHeartRef.current.offsetHeight;

        // 获取2D上下文
        const ctx = gardenCanvas.getContext('2d');
        if (!ctx) return;

        // 设置混合模式
        ctx.globalCompositeOperation = "lighter";
        
        // 创建花园实例
        gardenRef.current = new Garden(ctx, gardenCanvas);

        // 调整布局
        adjustLayout();

        // 花园渲染循环
        const renderInterval = setInterval(() => {
            gardenRef.current?.render();
        }, Garden.options.growSpeed);
        animationRefs.current.intervals.push(renderInterval);

        // 启动代码打字效果
        typeWriterCodeContent();

        // 光标闪烁效果
        const cursorInterval = setInterval(() => {
            if (clearValRef.current) {
                setShowCursor(prev => !prev);
            } else {
                clearInterval(cursorInterval);
                setShowCursor(false);
            }
        }, 600);
        animationRefs.current.intervals.push(cursorInterval);

        // 5秒后开始心形动画
        const heartTimeout = setTimeout(() => {
            startHeartAnimation();
        }, 5000);
        animationRefs.current.timeouts.push(heartTimeout);

        // 初始化计时器
        timeElapse(startDate);
        const timeInterval = setInterval(() => timeElapse(startDate), 500);
        animationRefs.current.intervals.push(timeInterval);

        // 窗口大小变化监听
        const handleResize = () => adjustLayout();
        window.addEventListener('resize', handleResize);

        // 清理函数
        return () => {
            animationRefs.current.intervals.forEach(interval => clearInterval(interval));
            animationRefs.current.timeouts.forEach(timeout => clearTimeout(timeout));
            window.removeEventListener('resize', handleResize);
        };
    }, [startDate]);

    // 显示消息后的副作用
    useEffect(() => {
        if (showMessages) {
            adjustWordsPosition();
            const timer = setTimeout(() => setShowLoveU(true), 5000);
            animationRefs.current.timeouts.push(timer);
            return () => clearTimeout(timer);
        }
    }, [showMessages]);

    // 显示告白后的副作用
    useEffect(() => {
        if (showLoveU && loveURef.current) {
            const loveUContent = `${messages.love}<br/><div class='signature'>${messages.signature}</div>`;
            loveURef.current.innerHTML = '';
            typeWriter(loveURef.current, loveUContent, 75);
        }
    }, [showLoveU, messages]);

    // 同步clearVal状态到ref
    useEffect(() => {
        clearValRef.current = clearVal;
    }, [clearVal]);

    // ========== 工具函数 ==========

    /**
     * 代码打字效果
     */
    const typeWriterCodeContent = () => {
        setShowCursor(true);
        let i = 0;
        const speed = 10; // 打字速度(毫秒/字符)
        
        const typing = setInterval(() => {
            if (i < fullCodeContent.length) {
                setCodeContent(fullCodeContent.substring(0, i + 1));
                i++;
            } else {
                clearInterval(typing);
                setClearVal(false); // 打字完成,停止光标闪烁
            }
        }, speed);
        animationRefs.current.intervals.push(typing);
    };

    /**
     * 计算心形曲线上的点
     * @param angle 角度(弧度)
     * @returns [x, y]坐标
     */
    const getHeartPoint = (angle: number): [number, number] => {
        // 心形曲线参数方程
        const x = 19.5 * (16 * Math.pow(Math.sin(angle), 3));
        const y = -20 * (13 * Math.cos(angle) - 5 * Math.cos(2 * angle) - 2 * Math.cos(3 * angle) - Math.cos(4 * angle));
        
        // 计算相对于心形容器中心的坐标
        const offsetX = loveHeartRef.current?.offsetWidth ? loveHeartRef.current.offsetWidth / 2 : 0;
        const offsetY = loveHeartRef.current?.offsetHeight ? loveHeartRef.current.offsetHeight / 2 - 55 : 0;
        
        return [offsetX + x, offsetY + y];
    };

    /**
     * 开始心形动画
     */
    const startHeartAnimation = () => {
        const interval = 50; // 花朵生成间隔(毫秒)
        const speed = 0.2;   // 角度变化速度
        let angle = 10;      // 起始角度
        const points: [number, number][] = []; // 已生成的点

        const animation = setInterval(() => {
            const point = getHeartPoint(angle);
            let valid = true;

            // 检查新点与已有点的距离
            for (const p of points) {
                const distance = Math.sqrt(Math.pow(p[0] - point[0], 2) + Math.pow(p[1] - point[1], 2));
                if (distance < Garden.options.bloomRadius.max * 1.3) {
                    valid = false;
                    break;
                }
            }

            // 如果点有效,创建花朵
            if (valid && gardenRef.current) {
                points.push(point);
                gardenRef.current.createRandomBloom(point[0], point[1]);
            }

            // 动画结束条件
            if (angle >= 30) {
                clearInterval(animation);
                setShowMessages(true); // 显示消息
            } else {
                angle += speed; // 继续动画
            }
        }, interval);
        animationRefs.current.intervals.push(animation);
    };

    /**
     * 通用打字机效果
     * @param element 目标DOM元素
     * @param text 要显示的文本
     * @param speed 打字速度(毫秒/字符)
     */
    const typeWriter = (element: HTMLElement, text: string, speed: number) => {
        let i = 0;
        element.innerHTML = '';

        const typing = setInterval(() => {
            if (i < text.length) {
                const char = text.substr(i, 1);
                // 跳过HTML标签
                if (char === '<') {
                    const closingIndex = text.indexOf('>', i);
                    i = closingIndex === -1 ? text.length : closingIndex + 1;
                } else {
                    i++;
                }
                // 更新内容并添加光标
                element.innerHTML = text.substring(0, i) + (i % 2 ? '_' : '');
            } else {
                clearInterval(typing);
            }
        }, speed);
        animationRefs.current.intervals.push(typing);
    };

    /**
     * 计算并显示恋爱时长
     * @param date 开始日期
     */
    const timeElapse = (date: Date) => {
        if (!elapseClockRef.current) return;

        const now = new Date();
        const seconds = (now.getTime() - date.getTime()) / 1000;

        // 计算天数
        const days = Math.floor(seconds / (3600 * 24));
        let remaining = seconds % (3600 * 24);
        
        // 计算小时
        const hours = Math.floor(remaining / 3600);
        remaining %= 3600;
        
        // 计算分钟
        const minutes = Math.floor(remaining / 60);
        remaining %= 60;
        
        // 格式化显示(补零)
        const formattedHours = hours < 10 ? `0${hours}` : hours.toString();
        const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes.toString();
        const formattedSeconds = remaining < 10 ? `0${Math.floor(remaining)}` : Math.floor(remaining).toString();

        // 更新DOM
        elapseClockRef.current.innerHTML = `
            <span class="digit">${days}</span> 天 
            <span class="digit">${formattedHours}</span> 小时 
            <span class="digit">${formattedMinutes}</span> 分钟 
            <span class="digit">${formattedSeconds}</span> 秒
        `;
    };

    /**
     * 调整文字位置
     */
    const adjustWordsPosition = () => {
        if (!wordsRef.current || !canvasRef.current) return;
        const garden = canvasRef.current;
        const words = wordsRef.current;

        words.style.position = 'absolute';
        words.style.top = `${garden.offsetTop + 195}px`;
        words.style.left = `${garden.offsetLeft + 70}px`;
    };

    /**
     * 调整代码区域位置
     */
    const adjustCodePosition = () => {
        if (!codeRef.current || !canvasRef.current) return;
        const garden = canvasRef.current;
        const code = codeRef.current;

        code.style.marginTop = `${(garden.offsetHeight - code.offsetHeight) / 2}px`;
    };

    /**
     * 响应式布局调整
     */
    const adjustLayout = () => {
        if (!contentRef.current || !loveHeartRef.current || !codeRef.current) return;

        const content = contentRef.current;
        const loveHeart = loveHeartRef.current;
        const code = codeRef.current;

        // 计算合适尺寸
        const width = loveHeart.offsetWidth + code.offsetWidth;
        const height = Math.max(loveHeart.offsetHeight, code.offsetHeight);

        // 设置容器尺寸(考虑窗口边界)
        content.style.width = `${Math.min(width, window.innerWidth - 40)}px`;
        content.style.height = `${Math.min(height, window.innerHeight - 40)}px`;
        
        // 居中显示
        content.style.marginTop = `${Math.max((window.innerHeight - content.offsetHeight) / 2, 10)}px`;
        content.style.marginLeft = `${Math.max((window.innerWidth - content.offsetWidth) / 2, 10)}px`;

        // 调整代码区域垂直居中
        adjustCodePosition();
    };

    /**
     * 渲染代码区域
     */
    const renderCodeContent = () => {
        return (
            <div id="code" ref={codeRef}>
                {/* 使用dangerouslySetInnerHTML显示带HTML格式的代码 */}
                <div dangerouslySetInnerHTML={{ __html: codeContent }} />
                {/* 闪烁的光标(心形) */}
                {showCursor && (
                    <span className="heart-cursor" style={{ color: 'red' }}>♥</span>
                )}
            </div>
        );
    };

    // ========== 组件渲染 ==========
    return (
        <div className="btnbg lovePage">
            {/* 背景层 */}
            <div id="mainDiv">
                {/* 主内容容器 */}
                <div id="content" ref={contentRef}>
                    {/* 左侧:代码区域 */}
                    {renderCodeContent()}
                    
                    {/* 右侧:心形动画区域 */}
                    <div id="loveHeart" ref={loveHeartRef}>
                        {/* 花园画布 */}
                        <canvas id="garden" ref={canvasRef}></canvas>
                        
                        {/* 情话文本区域(默认隐藏) */}
                        <div
                            id="words"
                            ref={wordsRef}
                            style={{
                                display: showMessages ? 'block' : 'none',
                                opacity: showMessages ? 1 : 0,
                                transition: 'opacity 1s ease-in-out'
                            }}
                        >
                            {/* 初始消息 */}
                            <div id="messages" ref={messagesRef}>
                                {messages.initial}
                                {/* 恋爱计时器 */}
                                <div id="elapseClock" ref={elapseClockRef}></div>
                            </div>
                            
                            {/* 最终告白(默认隐藏) */}
                            <div
                                id="loveu"
                                ref={loveURef}
                                style={{
                                    display: showLoveU ? 'block' : 'none',
                                    opacity: showLoveU ? 1 : 0,
                                    transition: 'opacity 1s ease-in-out'
                                }}
                            />
                        </div>
                    </div>
                </div>
            </div>
            
            {/* 浏览器兼容性错误提示 */}
            <div id="errorMsg" ref={errorMsgRef}></div>
        </div>
    );
};

export default LoveAnimation;

GardenClasses.ts文件:

javascript 复制代码
// GardenClasses.ts
export interface VectorProps {
  x: number;
  y: number;
}

export interface PetalOptions {
  stretchA: number;
  stretchB: number;
  startAngle: number;
  angle: number;
  growFactor: number;
  bloom: Bloom;
}

export interface BloomOptions {
  p: Vector;
  r: number;
  c: string;
  pc: number;
  garden: Garden;
}

export interface GardenOptions {
  petalCount: { min: number; max: number };
  petalStretch: { min: number; max: number };
  growFactor: { min: number; max: number };
  bloomRadius: { min: number; max: number };
  density: number;
  growSpeed: number;
  color: {
    rmin: number;
    rmax: number;
    gmin: number;
    gmax: number;
    bmin: number;
    bmax: number;
    opacity: number;
  };
  tanAngle: number;
}

export class Vector {
  x: number;
  y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }

  rotate(angle: number): Vector {
    const x = this.x;
    const y = this.y;
    this.x = Math.cos(angle) * x - Math.sin(angle) * y;
    this.y = Math.sin(angle) * x + Math.cos(angle) * y;
    return this;
  }

  mult(factor: number): Vector {
    this.x *= factor;
    this.y *= factor;
    return this;
  }

  clone(): Vector {
    return new Vector(this.x, this.y);
  }

  length(): number {
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }

  subtract(v: Vector): Vector {
    this.x -= v.x;
    this.y -= v.y;
    return this;
  }

  set(x: number, y: number): Vector {
    this.x = x;
    this.y = y;
    return this;
  }
}

export class Petal {
  stretchA: number;
  stretchB: number;
  startAngle: number;
  angle: number;
  bloom: Bloom;
  growFactor: number;
  r: number;
  isfinished: boolean;

  constructor(options: PetalOptions) {
    this.stretchA = options.stretchA;
    this.stretchB = options.stretchB;
    this.startAngle = options.startAngle;
    this.angle = options.angle;
    this.bloom = options.bloom;
    this.growFactor = options.growFactor;
    this.r = 1;
    this.isfinished = false;
  }

  draw(): void {
    const ctx = this.bloom.garden.ctx;
    const e = new Vector(0, this.r).rotate(Garden.degrad(this.startAngle));
    const d = e.clone().rotate(Garden.degrad(this.angle));
    const c = e.clone().mult(this.stretchA);
    const b = d.clone().mult(this.stretchB);

    ctx.strokeStyle = this.bloom.c;
    ctx.beginPath();
    ctx.moveTo(e.x, e.y);
    ctx.bezierCurveTo(c.x, c.y, b.x, b.y, d.x, d.y);
    ctx.stroke();
  }

  render(): void {
    if (this.r <= this.bloom.r) {
      this.r += this.growFactor;
      this.draw();
    } else {
      this.isfinished = true;
    }
  }
}

export class Bloom {
  p: Vector;
  r: number;
  c: string;
  pc: number;
  petals: Petal[];
  garden: Garden;

  constructor(options: BloomOptions) {
    this.p = options.p;
    this.r = options.r;
    this.c = options.c;
    this.pc = options.pc;
    this.petals = [];
    this.garden = options.garden;
    this.init();
    this.garden.addBloom(this);
  }

  draw(): void {
    let isFinished = true;
    this.garden.ctx.save();
    this.garden.ctx.translate(this.p.x, this.p.y);

    for (const petal of this.petals) {
      petal.render();
      isFinished = isFinished && petal.isfinished;
    }

    this.garden.ctx.restore();
    if (isFinished) {
      this.garden.removeBloom(this);
    }
  }

  init(): void {
    const angle = 360 / this.pc;
    const startAngle = Garden.randomInt(0, 90);

    for (let i = 0; i < this.pc; i++) {
      this.petals.push(
        new Petal({
          stretchA: Garden.random(Garden.options.petalStretch.min, Garden.options.petalStretch.max),
          stretchB: Garden.random(Garden.options.petalStretch.min, Garden.options.petalStretch.max),
          startAngle: startAngle + i * angle,
          angle: angle,
          growFactor: Garden.random(Garden.options.growFactor.min, Garden.options.growFactor.max),
          bloom: this,
        })
      );
    }
  }
}

export class Garden {
  blooms: Bloom[];
  element: HTMLCanvasElement;
  ctx: CanvasRenderingContext2D;
  static options: GardenOptions = {
    petalCount: { min: 8, max: 15 },
    petalStretch: { min: 0.1, max: 3 },
    growFactor: { min: 0.1, max: 1 },
    bloomRadius: { min: 8, max: 10 },
    density: 10,
    growSpeed: 1000 / 60,
    color: {
      rmin: 128,
      rmax: 255,
      gmin: 0,
      gmax: 128,
      bmin: 0,
      bmax: 128,
      opacity: 0.1,
    },
    tanAngle: 60,
  };

  constructor(ctx: CanvasRenderingContext2D, element: HTMLCanvasElement) {
    this.blooms = [];
    this.element = element;
    this.ctx = ctx;
  }

  render(): void {
    for (const bloom of this.blooms) {
      bloom.draw();
    }
  }

  addBloom(bloom: Bloom): void {
    this.blooms.push(bloom);
  }

  removeBloom(bloom: Bloom): void {
    const index = this.blooms.indexOf(bloom);
    if (index !== -1) {
      this.blooms.splice(index, 1);
    }
  }

  createRandomBloom(x: number, y: number): void {
    this.createBloom(
      x,
      y,
      Garden.randomInt(Garden.options.bloomRadius.min, Garden.options.bloomRadius.max),
      Garden.randomrgba(
        Garden.options.color.rmin,
        Garden.options.color.rmax,
        Garden.options.color.gmin,
        Garden.options.color.gmax,
        Garden.options.color.bmin,
        Garden.options.color.bmax,
        Garden.options.color.opacity
      ),
      Garden.randomInt(Garden.options.petalCount.min, Garden.options.petalCount.max)
    );
  }

  createBloom(x: number, y: number, radius: number, color: string, petalCount: number): void {
    new Bloom({
      p: new Vector(x, y),
      r: radius,
      c: color,
      pc: petalCount,
      garden: this,
    });
  }

  clear(): void {
    this.blooms = [];
    this.ctx.clearRect(0, 0, this.element.width, this.element.height);
  }

  static random(min: number, max: number): number {
    return Math.random() * (max - min) + min;
  }

  static randomInt(min: number, max: number): number {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }

  static readonly circle = 2 * Math.PI;

  static degrad(angle: number): number {
    return (Garden.circle / 360) * angle;
  }

  static raddeg(angle: number): number {
    return (angle / Garden.circle) * 360;
  }

  static rgba(r: number, g: number, b: number, a: number): string {
    return `rgba(${r},${g},${b},${a})`;
  }

  static randomrgba(
    rmin: number,
    rmax: number,
    gmin: number,
    gmax: number,
    bmin: number,
    bmax: number,
    a: number
  ): string {
    const r = Math.round(Garden.random(rmin, rmax));
    const g = Math.round(Garden.random(gmin, gmax));
    const b = Math.round(Garden.random(bmin, bmax));
    const threshold = 5;

    if (
      Math.abs(r - g) <= threshold &&
      Math.abs(g - b) <= threshold &&
      Math.abs(b - r) <= threshold
    ) {
      return Garden.rgba(rmin, rmax, gmin, gmax, bmin, bmax, a);
    } else {
      return Garden.rgba(r, g, b, a);
    }
  }
}

Love.less

javascript 复制代码
// 主色调(基于 #ffc0cb 扩展的渐变色系)
@color-1: #ffc0cb; // 粉红
@color-2: #ffb6c1; // 稍暗的粉
@color-3: #ffd1dc; // 浅粉
@color-4: #ffdfed; // 更浅的粉
@color-5: #ffecf2; // 接近白色
@font-face {
    font-family: digit;
    src: url('digital-7_mono.ttf') format("truetype");
}
// 动画定义
.keyframes() {
    @keyframes gentleFlow {
        0% {
            background-position: 0% 50%;
        }

        50% {
            background-position: 100% 50%;
        }

        100% {
            background-position: 0% 50%;
        }
    }
}

// 主背景样式
.lovePage {
    min-height: 100vh;
    background: linear-gradient(45deg,
            @color-1,
            @color-2,
            @color-3,
            @color-4,
            @color-5,
            @color-1 );
    background-size: 300% 300%;
    animation: gentleFlow 12s ease infinite;
    position: relative;
    overflow: hidden;

    .keyframes();

    // 光斑效果(增强层次感)
    &::before {
        content: '';
        position: absolute;
        width: 200%;
        height: 200%;
        background:
            radial-gradient(circle at 70% 20%, rgba(255, 255, 255, 0.2) 0%, transparent 30%),
            radial-gradient(circle at 30% 80%, rgba(255, 255, 255, 0.15) 0%, transparent 30%);
        animation: gentleFlow 20s linear infinite reverse;
    }

}

canvas {
    padding: 0;
    margin: 0;
}

div.btnbg {
    width: 100%;
    height: 100%;

}

#code,#messages,#loveu{
    color: #333;
}
#mainDiv {
    width: 100%;
    height: 100%
}

#loveHeart {
    width: 670px;
    height: 625px
}

#garden {
    width: 100%;
    height: 100%
}

#elapseClock {
    text-align: right;
    font-size: 18px;
    margin-top: 10px;
    margin-bottom: 10px
}

#words {
    font-family: "sans-serif";
    width: 500px;
    font-size: 24px;
    color: #666
}

#elapseClock .digit {
    font-family: "digit";
    font-size: 36px
}

#loveu {
    padding: 5px;
    font-size: 22px;
    margin-top: 40px;
    margin-right: 120px;
    text-align: right;
    display: none
}

#loveu .signature {
    margin-top: 10px;
    font-size: 20px;
    font-style: italic
}

#clickSound {
    display: none
}
#content{
    display: flex;
    justify-content: center;
    align-items: center;
}

#code {
    width: 440px;
    height: 400px;
    color: #333;
    font-family: "Consolas","Monaco","Bitstream Vera Sans Mono","Courier New","sans-serif";
    font-size: 12px;
    margin: 0 !important;
}

.string {
    color: #2a36ff
}

.keyword {
    color: #7f0055;
    font-weight: bold
}

.placeholder {
    margin-left: 15px
}

.space {
    margin-left: 7px
}

.comments {
    color: #3f7f5f
}

#copyright {
    margin-top: 10px;
    text-align: center;
    width: 100%;
    color: #666
}

#errorMsg {
    width: 100%;
    text-align: center;
    font-size: 24px;
    position: absolute;
    top: 100px;
    left: 0
}

#copyright a {
    color: #666
}
.heart-cursor {
    animation: blink 1s infinite;
    font-size: 1em;
    vertical-align: middle;
}

@keyframes blink {
    0%, 100% { opacity: 1; }
    50% { opacity: 0; }
}
相关推荐
Qredsun10 分钟前
vue--ofd/pdf预览实现
前端·vue.js·pdf
BillKu6 小时前
Vue3 + Element Plus 中修改表格当前选中行的颜色
前端·vue.js·elementui
BillKu6 小时前
Axios中POST、PUT、PATCH用法区别
前端·vue.js
好奇的菜鸟7 小时前
掌握 npm 核心操作:从安装到管理依赖的完整指南
前端·npm·node.js
肥肠可耐的西西公主8 小时前
前端(小程序)学习笔记(CLASS 2):WXML模板语法与WXSS模板样式
前端·学习·小程序
逆袭的菜鸟X9 小时前
RxJS 高阶映射操作符详解:map、mergeMap 和 switchMap
前端
bubiyoushang8889 小时前
HTML5的新语义化标签
前端·html·html5
会飞的鱼先生10 小时前
vue3自定义指令来实现 v-copy 功能
前端·javascript·vue.js
陈天伟教授10 小时前
Web前端开发 - 制作简单的焦点图效果
java·开发语言·前端·前端开发·visual studio
_殊途10 小时前
前端三件套之html详解
前端·html