HTML&CSS : canvas和 JavaScript 打造动态爱心

538 阅读3分钟

这个页面通过 canvas 和 JavaScript 实现了一个动态的爱心动画效果。每个爱心的大小、颜色和位置会根据时间动态变化,形成一种动态的视觉效果。用户可以通过点击画布重新生成动画。


大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信我,我会发送完整的压缩包给你。

演示效果

HTML&CSS


<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>公众号关注:前端Hardy</title>
    <style>
        body {
            display: flex;
            align-items: center;
            justify-content: center;
            height: 100vh;
            background: #009999;
        }
    </style>
</head>

<body>
    <script>
        console.clear();
        let canvas, canvasCtx;
        let canvasSize = [0, 0], scale = 1;
        let state;
        requestAnimationFrame(main);
        function main() {
            canvas = document.createElement('canvas');
            document.body.appendChild(canvas);
            document.body.style.margin = '0';
            canvas.style.display = 'block';
            canvasCtx = canvas.getContext('2d');
            checkResizeAndInit();
            reset();

            canvas.addEventListener('mousemove', (e) => {
                state.pointer.pos[0] = e.offsetX;
                state.pointer.pos[1] = e.offsetY;
            });
            canvas.addEventListener('click', reset);
            window.addEventListener('resize', reset);
            requestAnimationFrame(mainLoop);
            function mainLoop() {
                tick();
                requestAnimationFrame(mainLoop);
            }
        }
        function reset() {
            state = {
                time: 0,
                timeDelta: 1 / 60,
                pointer: { pos: [0, 0] },
                hearts: [],
            };
            const min = Math.min(canvasSize[0], canvasSize[1]);
            const step = min / 13;
            const center = [
                canvasSize[0] / 2,
                canvasSize[1] / 2
            ];
            drawHeart(
                center,
                min,
                Math.PI * state.time * 0,
                null, null,
            );
            state.hearts.length = 0;
            for (let y = 0; y < canvasSize[1]; y += step) {
                for (let x = 0; x < canvasSize[0]; x += step) {
                    const isInside = canvasCtx.isPointInPath(x, y);
                    if (!isInside) continue;
                    const dist = Math.hypot(x - center[0], y - center[1]);
                    state.hearts.push({
                        pos: [
                            x + step * Math.random() * 0.25,
                            y + step * Math.random() * 0.25,
                        ],
                        w: step * (0.25 + 0.75 * (1 - (dist / min))),
                        a: (Math.random() - 0.5) * Math.PI * 0.5,
                        timeOffset: (1 - (dist / min)) * 2,
                        timeScale: 1,
                        color1: '#ff0000',
                        color2: '#ff00ff',
                    });
                }
            }
        }

        function tick() {
            checkResizeAndInit();
            canvasCtx.fillStyle = `rgba(0, 0, 0, ${1})`;
            canvasCtx.fillRect(0, 0, canvasSize[0], canvasSize[1]);
            doIt();
            state.time += state.timeDelta;

        }
        function checkResizeAndInit() {
            if (
                window.innerWidth === canvasSize[0] &&
                window.innerHeight === canvasSize[1]
            ) return;
            canvasSize[0] = canvas.width = window.innerWidth;
            canvasSize[1] = canvas.height = window.innerHeight;
        }
        function doIt() {
            const { timeDelta } = state;
            for (const heart of state.hearts) {
                const time = state.time * heart.timeScale + heart.timeOffset;
                const s = ((Math.sin(time * Math.PI) + 1) / 2) ** 0.5 * 0.5 + 0.5;
                drawHeart(
                    heart.pos,
                    heart.w * s,
                    heart.a,
                    `color-mix(in hsl, ${heart.color1}, ${heart.color2} ${100 * Math.abs(((time / 3) % 2) - 1)}%)`,
                );
            }
            if (0) {
                const s = ((Math.sin(state.time * Math.PI) + 1) / 2) ** 0.5 * 0.5 + 0.5;
                drawHeart(
                    [canvasSize[0] / 2, canvasSize[1] / 2],
                    256 * s,
                    Math.PI * state.time * 0,
                    'color-mix(in hsl, #00ff00, #ff0000 50%)',
                );
            }
        }
        function drawHeart(pos, w, a, color, color2) {
            const s = w / 92;
            canvasCtx.save();
            canvasCtx.translate(pos[0], pos[1]);
            canvasCtx.rotate(a);
            canvasCtx.translate(0, 12 * s);
            canvasCtx.beginPath();
            canvasCtx.ellipse(
                -14 * s, -16 * s,
                25 * s, 32 * s,
                Math.PI * -0.25,
                Math.PI * 1, Math.PI * 0,
            );
            canvasCtx.ellipse(
                +14 * s, -16 * s,
                25 * s, 32 * s,
                Math.PI * +0.25,
                Math.PI * 1, Math.PI * 0,
            );
            canvasCtx.quadraticCurveTo(
                +14 * s, 20 * s,
                0 * s, 32 * s,
            );
            canvasCtx.closePath();
            if (color) {
                canvasCtx.strokeStyle = color;
                canvasCtx.stroke();
            }
            if (color2) {
                canvasCtx.fillStyle = color2;
                canvasCtx.fill();
            }
            canvasCtx.restore();
        }

        function drawSkull(pos, w, a, color, color2) {
            const s = w / 92;
            canvasCtx.save();
            canvasCtx.translate(pos[0], pos[1]);
            canvasCtx.rotate(a);
            canvasCtx.strokeStyle = '#7f7f7f';
            canvasCtx.beginPath();
            canvasCtx.arc(
                0, 0,
                w * 0.5,
                0, Math.PI * 2,
            );
            canvasCtx.stroke();
            canvasCtx.translate(0, 12 * s);
            canvasCtx.beginPath();
            canvasCtx.ellipse(
                0 * s, -20 * s,
                44 * s, 32 * s,
                Math.PI * 0,
                Math.PI * 0.70, Math.PI * 0.3,
            );
            canvasCtx.lineTo(+20 * s, 28 * s);
            canvasCtx.lineTo(+20 * s, 0 * s);
            canvasCtx.arc(
                0 * s, -10 * s,
                10 * s,
                Math.PI * 0.4, Math.PI * -1.4,
                true,
            );
            canvasCtx.lineTo(-20 * s, 28 * s);
            canvasCtx.closePath();
            if (color) {
                canvasCtx.strokeStyle = color;
                canvasCtx.stroke();
            }
            if (color2) {
                canvasCtx.fillStyle = color2;
                canvasCtx.fill();
            }
            canvasCtx.restore();
        }
        function rotateByVector(out, a, v, origin, s) {
            const rx = v[0];
            const ry = v[1];
            const x = a[0] - origin[0];
            const y = a[1] - origin[1];
            out[0] = origin[0] + (x * rx - y * ry) * s;
            out[1] = origin[1] + (y * rx + x * ry) * s;
            return out;
        }
    </script>
</body>

</html>

初始化和主循环

requestAnimationFrame(main);
function main() {
    canvas = document.createElement('canvas');
    document.body.appendChild(canvas);
    document.body.style.margin = '0';
    canvas.style.display = 'block';
    canvasCtx = canvas.getContext('2d');
    checkResizeAndInit();
    reset();

    canvas.addEventListener('mousemove', (e) => {
        state.pointer.pos[0] = e.offsetX;
        state.pointer.pos[1] = e.offsetY;
    });
    canvas.addEventListener('click', reset);
    window.addEventListener('resize', reset);
    requestAnimationFrame(mainLoop);
    function mainLoop() {
        tick();
        requestAnimationFrame(mainLoop);
    }
}
  • main():初始化函数,创建 canvas 元素并附加到页面上。
  • 设置 canvas 的样式,使其全屏显示。
  • 初始化 canvas 的 2D 绘图上下文(canvasCtx)。
  • 添加事件监听器,用于处理鼠标移动、点击和窗口大小调整。
  • 启动主循环 mainLoop(),通过 requestAnimationFrame 实现持续的动画效果。

状态管理

function reset() {
    state = {
        time: 0,
        timeDelta: 1 / 60,
        pointer: { pos: [0, 0] },
        hearts: [],
    };
    const min = Math.min(canvasSize[0], canvasSize[1]);
    const step = min / 13;
    const center = [
        canvasSize[0] / 2,
        canvasSize[1] / 2
    ];
    drawHeart(
        center,
        min,
        Math.PI * state.time * 0,
        null, null,
    );
    state.hearts.length = 0;
    for (let y = 0; y < canvasSize[1]; y += step) {
        for (let x = 0; x < canvasSize[0]; x += step; x += step) {
            const isInside = canvasCtx.isPointInPath(x, y);
            if (!isInside) continue;
            const dist = Math.hypot(x - center[0], y - center[1]);
            state.hearts.push({
                pos: [
                    x + step * Math.random() * 0.25,
                    y + step * Math.random() * 0.25,
                ],
                w: step * (0.25 + 0.75 * (1 - (dist / min))),
                a: (Math.random() - 0.5) * Math.PI * 0.5,
                timeOffset: (1 - (dist / min)) * 2,
                timeScale: 1,
                color1: '#ff0000',
                color2: '#ff00ff',
            });
        }
    }
}

  • reset():初始化或重置动画状态。
  • 创建一个状态对象 state,包含时间、鼠标位置和爱心数组。
  • 使用 drawHeart() 函数绘制一个大爱心作为参考路径。
  • 遍历画布上的每个点,检查是否在爱心路径内,如果是,则创建一个小爱心对象并添加到数组中。
  • 每个小爱心的大小、位置和颜色会根据其与中心的距离动态计算。

动画更新

function tick() {
    checkResizeAndInit();
    canvasCtx.fillStyle = `rgba(0, 0, 0, ${1})`;
    canvasCtx.fillRect(0, 0, canvasSize[0], canvasSize[1]);
    doIt();
    state.time += state.timeDelta;
}
  • tick():每帧调用的函数,负责更新动画状态。
  • 检查窗口大小是否变化,并更新画布大小。
  • 清空画布,绘制新的帧。
  • 调用 doIt() 函数绘制所有爱心。
  • 更新时间。

爱心绘制

function drawHeart(pos, w, a, color, color2) {
    const s = w / 92;
    canvasCtx.save();
    canvasCtx.translate(pos[0], pos[1]);
    canvasCtx.rotate(a);
    canvasCtx.translate(0, 12 * s);
    canvasCtx.beginPath();
    canvasCtx.ellipse(
        -14 * s, -16 * s,
        25 * s, 32 * s,
        Math.PI * -0.25,
        Math.PI * 1, Math.PI * 0,
    );
    canvasCtx.ellipse(
        +14 * s, -16 * s,
        25 * s, 32 * s,
        Math.PI * +0.25,
        Math.PI * 1, Math.PI * 0,
    );
    canvasCtx.quadraticCurveTo(
        +14 * s, 20 * s,
        0 * s, 32 * s,
    );
    canvasCtx.closePath();
    if (color) {
        canvasCtx.strokeStyle = color;
        canvasCtx.stroke();
    }
    if (color2) {
        canvasCtx.fillStyle = color2;
        canvasCtx.fill();
    }
    canvasCtx.restore();
}
  • drawHeart():绘制一个爱心形状。
  • 使用 translate 和 rotate 将画布的原点移动到指定位置并旋转。
  • 使用 ellipse 和 quadraticCurveTo 绘制爱心的两个半圆和连接部分。
  • 使用 stroke() 和 fill() 绘制边框和填充颜色。

动态效果

function doIt() {
    const { timeDelta } = state;
    for (const heart of state.hearts) {
        const time = state.time * heart.timeScale + heart.timeOffset;
        const s = ((Math.sin(time * Math.PI) + 1) / 2) ** 0.5 * 0.5 + 0.5;
        drawHeart(
            heart.pos,
            heart.w * s,
            heart.a,
            `color-mix(in hsl, ${heart.color1}, ${heart.color2} ${100 * Math.abs(((time / 3) % 2) - 1)}%)`,
        );
    }
}
  • doIt():绘制所有动态爱心。
  • 遍历 state.hearts 数组,根据当前时间和偏移量计算每个爱心的缩放比例。
  • 使用 color-mix 动态混合颜色。
  • 调用 drawHeart() 绘制每个爱心。

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!