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; }
}
相关推荐
拾光拾趣录3 分钟前
for..in 和 Object.keys 的区别:从“遍历对象属性的坑”说起
前端·javascript
OpenTiny社区14 分钟前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
编程猪猪侠43 分钟前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞1 小时前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路1 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到111 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构