实现一个可以将图片压缩成JPEG过程分五个步骤：
步骤一：图像分割
步骤二：颜色空间转换RGB->YCbCr.
步骤三：离散余弦变换
步骤四：数据量化
步骤五：哈弗曼编码
进行可视化的网页。要求需要将所有的代码写在同一个html代码文件中。
支持用户上传自己的图片，并对各个步骤进行详细的可视化过程。
支持用户调整图片质量，并可视化、动画化压缩的每个过程。
现在我有这么一段代码：
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>JPEG压缩过程可视化</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        .controls {
            margin-bottom: 20px;
        }
        .controls input[type="range"] {
            width: 100%;
        }
        .accordion {
            margin-bottom: 10px;
        }
        .accordion-item {
            border: 1px solid #ccc;
            border-radius: 4px;
            margin-bottom: 5px;
        }
        .accordion-header {
            padding: 10px;
            cursor: pointer;
            background-color: #f1f1f1;
            user-select: none;
        }
        .accordion-content {
            display: none;
            padding: 10px;
            border-top: 1px solid #ccc;
        }
        .active .accordion-content {
            display: block;
        }
        canvas {
            max-width: 100%;
            height: auto;
            margin-bottom: 10px;
            border: 1px solid #ddd;
        }
        pre {
            background-color: #f9f9f9;
            padding: 10px;
            overflow: auto;
            max-height: 200px;
        }
        .grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(50px, 1fr));
            gap: 2px;
        }
        .grid div {
            width: 100%;
            padding-bottom: 100%;
            position: relative;
        }
        .grid div span {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #000;
            opacity: 0.5;
        }
        #compressedImage {
            margin-top: 20px;
            border: 1px solid #ccc;
        }
        /* 动画效果 */
        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }
        .fade-in {
            animation: fadeIn 1s ease-in-out;
        }
    </style>
</head>
<body>
    <h1>JPEG压缩过程可视化</h1>
    <div class="controls">
        <input type="file" id="upload" accept="image/*"><br><br>
        <label for="quality">调整图片质量:</label>
        <input type="range" id="quality" min="1" max="100" value="50"><br><br>
        <button id="startCompression">开始压缩动画</button>
    </div>
    
    <div class="accordion">
        <div class="accordion-item">
            <div class="accordion-header">步骤一：图像分割</div>
            <div class="accordion-content">
                <canvas id="canvasStep1"></canvas>
            </div>
        </div>
        <div class="accordion-item">
            <div class="accordion-header">步骤二：颜色空间转换 (RGB → YCbCr)</div>
            <div class="accordion-content">
                <canvas id="canvasStep2_Y"></canvas>
                <canvas id="canvasStep2_Cb"></canvas>
                <canvas id="canvasStep2_Cr"></canvas>
            </div>
        </div>
        <div class="accordion-item">
            <div class="accordion-header">步骤三：离散余弦变换 (DCT)</div>
            <div class="accordion-content">
                <div id="canvasStep3" class="grid"></div>
            </div>
        </div>
        <div class="accordion-item">
            <div class="accordion-header">步骤四：数据量化</div>
            <div class="accordion-content">
                <div id="canvasStep4" class="grid"></div>
            </div>
        </div>
        <div class="accordion-item">
            <div class="accordion-header">步骤五：哈夫曼编码</div>
            <div class="accordion-content">
                <pre id="HuffmanCodes">哈夫曼编码将在这里显示</pre>
            </div>
        </div>
    </div>

    <h2>压缩后的图像</h2>
    <canvas id="compressedImage"></canvas>

    <script>
        // 获取HTML元素
        const upload = document.getElementById('upload');
        const qualitySlider = document.getElementById('quality');
        const startBtn = document.getElementById('startCompression');
        const accordionHeaders = document.querySelectorAll('.accordion-header');

        const canvasStep1 = document.getElementById('canvasStep1');
        const canvasStep2_Y = document.getElementById('canvasStep2_Y');
        const canvasStep2_Cb = document.getElementById('canvasStep2_Cb');
        const canvasStep2_Cr = document.getElementById('canvasStep2_Cr');
        const canvasStep3 = document.getElementById('canvasStep3');
        const canvasStep4 = document.getElementById('canvasStep4');
        const HuffmanCodes = document.getElementById('HuffmanCodes');
        const compressedImage = document.getElementById('compressedImage');

        let originalImage = new Image();
        let imageDataGlobal, Y, Cb, Cr;
        let qualityFactorGlobal = 50;
        let compressedDataGlobal = [];

        upload.addEventListener('change', handleUpload);
        qualitySlider.addEventListener('input', () => {
            // Update quality factor
            qualityFactorGlobal = parseInt(qualitySlider.value);
        });
        startBtn.addEventListener('click', () => {
            if (originalImage.src) {
                startCompressionAnimation();
            } else {
                alert('请先上传一张图片。');
            }
        });

        // 处理手风琴展开和折叠
        accordionHeaders.forEach(header => {
            header.addEventListener('click', () => {
                header.parentElement.classList.toggle('active');
            });
        });

        function handleUpload(event) {
            const file = event.target.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onload = function(e) {
                originalImage.src = e.target.result;
            }
            reader.readAsDataURL(file);
        }

        originalImage.onload = function() {
            // 自动打开前两个手风琴项
            const accordionItems = document.querySelectorAll('.accordion-item');
            accordionItems[0].classList.add('active');
            accordionItems[1].classList.add('active');
            processImage();
        }

        function startCompressionAnimation() {
            // Reset all steps
            const accordionItems = document.querySelectorAll('.accordion-item');
            accordionItems.forEach((item, index) => {
                if (index < 2) return; // Steps 1 and 2 are already open
                item.classList.remove('active');
            });

            // Start animation steps sequentially
            animateStep1()
                .then(() => animateStep2())
                .then(() => animateStep3())
                .then(() => animateStep4())
                .then(() => animateStep5())
                .then(() => showCompressedImage());
        }

        async function animateStep1() {
            return new Promise((resolve) => {
                const accordionItem = document.querySelectorAll('.accordion-item')[0];
                accordionItem.classList.add('active');
                const ctx = canvasStep1.getContext('2d');
                canvasStep1.width = originalImage.width;
                canvasStep1.height = originalImage.height;
                ctx.clearRect(0, 0, canvasStep1.width, canvasStep1.height);
                ctx.drawImage(originalImage, 0, 0);
                // 添加动画效果
                canvasStep1.classList.add('fade-in');
                setTimeout(() => {
                    resolve();
                }, 1000);
            });
        }

        async function animateStep2() {
            return new Promise((resolve) => {
                const accordionItem = document.querySelectorAll('.accordion-item')[1];
                accordionItem.classList.add('active');

                //转换颜色空间
                const offCanvas = document.createElement('canvas');
                const offCtx = offCanvas.getContext('2d');
                offCanvas.width = originalImage.width;
                offCanvas.height = originalImage.height;
                offCtx.drawImage(originalImage, 0, 0);
                imageDataGlobal = offCtx.getImageData(0, 0, offCanvas.width, offCanvas.height);
                const data = imageDataGlobal.data;

                Y = [];
                Cb = [];
                Cr = [];
                for (let i = 0; i < data.length; i += 4) {
                    const r = data[i];
                    const g = data[i+1];
                    const b = data[i+2];
                    // 标准转换公式
                    const y = 0.299*r + 0.587*g + 0.114*b;
                    const cb = -0.168736*r - 0.331264*g + 0.5*b + 128;
                    const cr = 0.5*r - 0.418688*g - 0.081312*b + 128;
                    Y.push(y);
                    Cb.push(cb);
                    Cr.push(cr);
                }

                // 显示Y, Cb, Cr
                displayChannel(canvasStep2_Y, Y, offCanvas.width, offCanvas.height, 'Y 分量');
                displayChannel(canvasStep2_Cb, Cb, offCanvas.width, offCanvas.height, 'Cb 分量');
                displayChannel(canvasStep2_Cr, Cr, offCanvas.width, offCanvas.height, 'Cr 分量');

                // 添加动画效果
                canvasStep2_Y.classList.add('fade-in');
                canvasStep2_Cb.classList.add('fade-in');
                canvasStep2_Cr.classList.add('fade-in');
                setTimeout(() => {
                    resolve();
                }, 1000);
            });
        }

        async function animateStep3() {
            return new Promise((resolve) => {
                const accordionItem = document.querySelectorAll('.accordion-item')[2];
                accordionItem.classList.add('active');

                // 清空之前的内容
                canvasStep3.innerHTML = '';

                // 对Y分量进行8x8块的2D DCT
                const blockSize = 8;
                const width = imageDataGlobal.width;
                const height = imageDataGlobal.height;
                const blocks = [];

                for (let y = 0; y < height; y += blockSize) {
                    for (let x = 0; x < width; x += blockSize) {
                        const block = [];
                        for (let i = 0; i < blockSize; i++) {
                            const row = [];
                            for (let j = 0; j < blockSize; j++) {
                                const idx = ((y + i) * width) + (x + j);
                                row.push(Y[idx]);
                            }
                            block.push(row);
                        }
                        blocks.push(block);
                    }
                }

                // 计算DCT for each block
                const dctBlocks = blocks.map(block => DCT2D(block));

                // 可视化DCT系数 (展示DC系数)
                dctBlocks.forEach((block, index) => {
                    const dc = block[0][0];
                    const normalized = normalizeValue(dc);
                    const div = document.createElement('div');
                    div.style.backgroundColor = `rgb(${255 - normalized}, ${255 - normalized}, ${255 - normalized})`;
                    canvasStep3.appendChild(div);
                });

                // 添加动画效果
                canvasStep3.classList.add('fade-in');
                // Store for later use
                compressedDataGlobal.dctBlocks = dctBlocks;
                setTimeout(() => {
                    resolve();
                }, 1000);
            });
        }

        async function animateStep4() {
            return new Promise((resolve) => {
                const accordionItem = document.querySelectorAll('.accordion-item')[3];
                accordionItem.classList.add('active');

                // 清空之前的内容
                canvasStep4.innerHTML = '';

                const blockSize = 8;
                const quantizedBlocks = compressedDataGlobal.dctBlocks.map(block => 
                    block.map(row => row.map(coef => Math.round(coef / (50 / qualityFactorGlobal)))
                ));

                quantizedBlocks.forEach(block => {
                    block.forEach(row => {
                        row.forEach(val => {
                            const normalized = normalizeValue(val);
                            const div = document.createElement('div');
                            div.style.backgroundColor = `rgb(${255 - normalized}, ${255 - normalized}, ${255 - normalized})`;
                            canvasStep4.appendChild(div);
                        });
                    });
                });

                // 添加动画效果
                canvasStep4.classList.add('fade-in');
                // Store for later use
                compressedDataGlobal.quantizedBlocks = quantizedBlocks;
                setTimeout(() => {
                    resolve();
                }, 1000);
            });
        }

        async function animateStep5() {
            return new Promise((resolve) => {
                const accordionItem = document.querySelectorAll('.accordion-item')[4];
                accordionItem.classList.add('active');

                // 哈夫曼编码
                const quantizedData = compressedDataGlobal.quantizedBlocks.flat(3);
                const freq = {};
                quantizedData.forEach(val => {
                    const key = val.toString();
                    freq[key] = (freq[key] || 0) + 1;
                });

                // 构建哈夫曼树
                const heap = [];
                for (let symbol in freq) {
                    heap.push({symbol: symbol, freq: freq[symbol]});
                }
                heap.sort((a, b) => a.freq - b.freq);
                while (heap.length > 1) {
                    const a = heap.shift();
                    const b = heap.shift();
                    heap.push({symbol: null, freq: a.freq + b.freq, left: a, right: b});
                    heap.sort((a, b) => a.freq - b.freq);
                }
                const huffmanTree = heap[0];
                const codes = {};
                function generateCodes(node, code) {
                    if (node.symbol !== null) {
                        codes[node.symbol] = code;
                        return;
                    }
                    generateCodes(node.left, code + '0');
                    generateCodes(node.right, code + '1');
                }
                generateCodes(huffmanTree, '');

                // 显示哈夫曼编码
                let codesText = '';
                for (let symbol in codes) {
                    codesText += `符号 ${symbol} : ${codes[symbol]}\n`;
                }
                HuffmanCodes.textContent = codesText;

                // 添加动画效果
                HuffmanCodes.classList.add('fade-in');
                setTimeout(() => {
                    resolve();
                }, 1000);
            });
        }

        function showCompressedImage() {
            // 简化的压缩恢复过程：反量化和逆DCT，仅用于展示
            const quantizedBlocks = compressedDataGlobal.quantizedBlocks;
            const dctBlocks = quantizedBlocks.map(block => 
                block.map(row => row.map(val => val * (50 / qualityFactorGlobal))
            ));
            const blocks = dctBlocks.map(block => IDCT2D(block));

            // 重建Y分量
            const width = imageDataGlobal.width;
            const height = imageDataGlobal.height;
            const reconstructedY = new Array(width * height).fill(0);

            let blockIndex = 0;
            for (let y = 0; y < height; y +=8) {
                for (let x = 0; x < width; x +=8) {
                    const block = blocks[blockIndex];
                    for (let i = 0; i < 8; i++) {
                        for (let j = 0; j < 8; j++) {
                            const idx = ((y + i) * width) + (x + j);
                            if(idx < reconstructedY.length){
                                reconstructedY[idx] = block[i][j];
                            }
                        }
                    }
                    blockIndex++;
                }
            }

            // 使用原始 Cb 和 Cr 来重建图像
            const offCanvas = document.createElement('canvas');
            const offCtx = offCanvas.getContext('2d');
            offCanvas.width = width;
            offCanvas.height = height;
            const reconstructedImageData = offCtx.createImageData(width, height);
            for (let i = 0; i < reconstructedY.length; i++) {
                const yVal = reconstructedY[i];
                const cbVal = Cb[i];
                const crVal = Cr[i];
                // YCbCr 转 RGB
                let r = yVal + 1.402 * (crVal - 128);
                let g = yVal - 0.344136 * (cbVal - 128) - 0.714136 * (crVal - 128);
                let b = yVal + 1.772 * (cbVal - 128);

                // Clamp to [0,255]
                r = Math.min(255, Math.max(0, Math.round(r)));
                g = Math.min(255, Math.max(0, Math.round(g)));
                b = Math.min(255, Math.max(0, Math.round(b)));

                reconstructedImageData.data[i*4] = r;
                reconstructedImageData.data[i*4 +1] = g;
                reconstructedImageData.data[i*4 +2] = b;
                reconstructedImageData.data[i*4 +3] = 255;
            }
            offCtx.putImageData(reconstructedImageData, 0, 0);

            // 显示重建后的图像
            compressedImage.width = width;
            compressedImage.height = height;
            const ctx = compressedImage.getContext('2d');
            ctx.clearRect(0, 0, compressedImage.width, compressedImage.height);
            ctx.drawImage(offCanvas, 0, 0);
            compressedImage.classList.add('fade-in');
        }

        function processImage() {
            // 初始化绘制，非动画模式下直接处理所有步骤
            // 此函数可以保持为空或用于即时处理
        }

        function displayChannel(canvas, data, width, height, label) {
            const ctx = canvas.getContext('2d');
            canvas.width = width;
            canvas.height = height;
            const imgData = ctx.createImageData(width, height);
            for (let i = 0; i < data.length; i++) {
                const val = Math.min(255, Math.max(0, data[i]));
                imgData.data[i*4] = val;
                imgData.data[i*4 +1] = val;
                imgData.data[i*4 +2] = val;
                imgData.data[i*4 +3] = 255;
            }
            ctx.putImageData(imgData, 0, 0);
            canvas.title = label;
        }

        function DCT2D(block) {
            const N = 8;
            const result = [];
            for (let u = 0; u < N; u++) {
                const row = [];
                for (let v = 0; v < N; v++) {
                    let sum = 0;
                    for (let x = 0; x < N; x++) {
                        for (let y = 0; y < N; y++) {
                            sum += block[x][y] *
                                   Math.cos(((2*x +1)*u*Math.PI)/(2*N)) *
                                   Math.cos(((2*y +1)*v*Math.PI)/(2*N));
                        }
                    }
                    let c_u = (u === 0) ? Math.sqrt(1/N) : Math.sqrt(2/N);
                    let c_v = (v === 0) ? Math.sqrt(1/N) : Math.sqrt(2/N);
                    row.push(c_u * c_v * sum);
                }
                result.push(row);
            }
            return result;
        }

        function IDCT2D(block) {
            const N = 8;
            const result = [];
            for (let x = 0; x < N; x++) {
                const row = [];
                for (let y = 0; y < N; y++) {
                    let sum = 0;
                    for (let u = 0; u < N; u++) {
                        for (let v = 0; v < N; v++) {
                            const c_u = (u === 0) ? Math.sqrt(1/N) : Math.sqrt(2/N);
                            const c_v = (v === 0) ? Math.sqrt(1/N) : Math.sqrt(2/N);
                            sum += c_u * c_v * block[u][v] *
                                   Math.cos(((2*x +1)*u*Math.PI)/(2*N)) *
                                   Math.cos(((2*y +1)*v*Math.PI)/(2*N));
                        }
                    }
                    row.push(sum);
                }
                result.push(row);
            }
            return result;
        }

        function normalizeValue(val) {
            // 简单归一化到0-255
            const absVal = Math.abs(val);
            const normalized = Math.min(255, Math.floor(absVal));
            return normalized;
        }
    </script>
</body>
</html>


这段代码的缺点是，我上传较大的图片后点击演示动画，整部电脑都会卡住，同时我注意到演示动画会出现一些太大的方框。我希望能为用户把图片调整到合适的大小再进行演示，同时能把动画元素缩小，能让用户一下子就看到所有正在发生的东西。