实现一个可以将图片压缩成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;
        }
        #compressedImage {
            margin-top: 20px;
            border: 1px solid #ccc;
            max-width: 100%;
            height: auto;
        }
        /* 动画效果 */
        @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">
                <canvas id="canvasStep3" width="400" height="400"></canvas>
            </div>
        </div>
        <div class="accordion-item">
            <div class="accordion-header">步骤四：数据量化</div>
            <div class="accordion-content">
                <canvas id="canvasStep4" width="400" height="400"></canvas>
            </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 maxDimension = 400; // 最大宽度或高度
            const { width, height } = resizeImage(originalImage, maxDimension);
            const resizedCanvas = document.createElement('canvas');
            resizedCanvas.width = width;
            resizedCanvas.height = height;
            const resizedCtx = resizedCanvas.getContext('2d');
            resizedCtx.drawImage(originalImage, 0, 0, width, height);
            originalImage = new Image();
            originalImage.src = resizedCanvas.toDataURL();
            originalImage.onload = function() {
                // 自动打开前两个手风琴项
                const accordionItems = document.querySelectorAll('.accordion-item');
                accordionItems[0].classList.add('active');
                accordionItems[1].classList.add('active');
                processImage();
            }
        }

        function resizeImage(img, maxDim) {
            let width = img.width;
            let height = img.height;
            if (width > height) {
                if (width > maxDim) {
                    height = Math.round(height * (maxDim / width));
                    width = maxDim;
                }
            } else {
                if (height > maxDim) {
                    width = Math.round(width * (maxDim / height));
                    height = maxDim;
                }
            }
            return { width, height };
        }

        function startCompressionAnimation() {
            // 重置所有步骤
            const accordionItems = document.querySelectorAll('.accordion-item');
            accordionItems.forEach((item, index) => {
                if (index < 5) { // 保持所有进行中的步骤
                    item.classList.remove('active');
                }
            });
            // 清除之前的内容
            clearAllSteps();

            // 开始依次执行压缩步骤
            animateStep1()
                .then(animateStep2)
                .then(animateStep3)
                .then(animateStep4)
                .then(animateStep5)
                .then(showCompressedImage)
                .catch(err => {
                    console.error('压缩过程出错:', err);
                });
        }

        function clearAllSteps() {
            canvasStep1.getContext('2d').clearRect(0, 0, canvasStep1.width, canvasStep1.height);
            canvasStep2_Y.getContext('2d').clearRect(0, 0, canvasStep2_Y.width, canvasStep2_Y.height);
            canvasStep2_Cb.getContext('2d').clearRect(0, 0, canvasStep2_Cb.width, canvasStep2_Cb.height);
            canvasStep2_Cr.getContext('2d').clearRect(0, 0, canvasStep2_Cr.width, canvasStep2_Cr.height);
            const ctx3 = canvasStep3.getContext('2d');
            ctx3.clearRect(0, 0, canvasStep3.width, canvasStep3.height);
            const ctx4 = canvasStep4.getContext('2d');
            ctx4.clearRect(0, 0, canvasStep4.width, canvasStep4.height);
            HuffmanCodes.textContent = '哈夫曼编码将在这里显示';
            compressedImage.getContext('2d').clearRect(0, 0, compressedImage.width, compressedImage.height);
        }

        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();
                }, 500); // 动画时间调整为500ms
            });
        }

        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();
                }, 500);
            });
        }

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

                // 清空之前的内容
                const ctx3 = canvasStep3.getContext('2d');
                ctx3.clearRect(0, 0, canvasStep3.width, canvasStep3.height);

                // 对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] - 128); // 按照JPEG标准减去128
                            }
                            block.push(row);
                        }
                        blocks.push(block);
                    }
                }

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

                // 可视化DCT系数 (展示DC系数)
                const dcCoefficients = dctBlocks.map(block => block[0][0]);

                // 找到DC系数的最小和最大值以进行归一化
                const minDC = Math.min(...dcCoefficients);
                const maxDC = Math.max(...dcCoefficients);

                // 绘制DC系数至步骤三的canvas
                const cols = Math.min(Math.floor(canvasStep3.width / 2), dcCoefficients.length);
                const rows = Math.ceil(dcCoefficients.length / cols);
                const cellSize = 2; // 每个DC系数的显示尺寸

                canvasStep3.width = cols * cellSize;
                canvasStep3.height = rows * cellSize;

                dcCoefficients.forEach((dc, index) => {
                    const normalized = ((dc - minDC) / (maxDC - minDC)) * 255;
                    const x = (index % cols) * cellSize;
                    const y = Math.floor(index / cols) * cellSize;
                    ctx3.fillStyle = `rgb(${normalized}, ${normalized}, ${normalized})`;
                    ctx3.fillRect(x, y, cellSize, cellSize);
                });

                // 存储DCT块用于后续步骤
                compressedDataGlobal.dctBlocks = dctBlocks;

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

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

                // 清空之前的内容
                const ctx4 = canvasStep4.getContext('2d');
                ctx4.clearRect(0, 0, canvasStep4.width, canvasStep4.height);

                const quantizationMatrix = getStandardQuantizationMatrix(qualityFactorGlobal);
                const dctBlocks = compressedDataGlobal.dctBlocks;
                const quantizedBlocks = [];

                // 量化
                dctBlocks.forEach(block => {
                    const qBlock = block.map((row, i) => 
                        row.map((val, j) => Math.round(val / quantizationMatrix[i][j]))
                    );
                    quantizedBlocks.push(qBlock);
                });

                compressedDataGlobal.quantizedBlocks = quantizedBlocks;

                // 可视化量化后的DC系数
                const quantizedDC = quantizedBlocks.map(block => block[0][0]);

                // 找到量化后DC系数的最小和最大值以进行归一化
                const minQDC = Math.min(...quantizedDC);
                const maxQDC = Math.max(...quantizedDC);

                const cols = Math.min(Math.floor(canvasStep4.width / 2), quantizedDC.length);
                const rows = Math.ceil(quantizedDC.length / cols);
                const cellSize = 2; // 每个DC系数的显示尺寸

                canvasStep4.width = cols * cellSize;
                canvasStep4.height = rows * cellSize;

                quantizedDC.forEach((dc, index) => {
                    const normalized = ((dc - minQDC) / (maxQDC - minQDC)) * 255;
                    const x = (index % cols) * cellSize;
                    const y = Math.floor(index / cols) * cellSize;
                    ctx4.fillStyle = `rgb(${normalized}, ${normalized}, ${normalized})`;
                    ctx4.fillRect(x, y, cellSize, cellSize);
                });

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

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

                // 哈夫曼编码
                const quantizedData = compressedDataGlobal.quantizedBlocks.flat(2);
                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, '');

                // 显示哈夫曼编码（限制显示前100个符号以避免过长）
                let codesText = '';
                let count = 0;
                for (let symbol in codes) {
                    codesText += `符号 ${symbol} : ${codes[symbol]}\n`;
                    count++;
                    if (count >= 100) {
                        codesText += `... 共 ${Object.keys(codes).length} 个符号`;
                        break;
                    }
                }
                HuffmanCodes.textContent = codesText;

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

        function showCompressedImage() {
            // 反量化和逆DCT
            const quantizedBlocks = compressedDataGlobal.quantizedBlocks;
            const quantizationMatrix = getStandardQuantizationMatrix(qualityFactorGlobal);
            const dctBlocks = quantizedBlocks.map(block => 
                block.map((row, i) => 
                    row.map((val, j) => val * quantizationMatrix[i][j])
                )
            );

            const idctBlocks = 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 = idctBlocks[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] + 128; // 重新加上128
                            }
                        }
                    }
                    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++) {
                let yVal = reconstructedY[i];
                let cbVal = Cb[i];
                let crVal = Cr[i];
                // Clamp Y to [0, 255]
                yVal = Math.min(255, Math.max(0, yVal));
                // 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 scaledVal = Math.min(255, Math.floor(absVal * 5)); // 放大系数以更明显显示
            return scaledVal;
        }

        function visualizeDCT(dctBlocks, width, height) {
            // 仅展示每个块的前1个系数（DC系数）
            dctBlocks.forEach(block => {
                const dc = block[0][0];
                const normalized = normalizeValue(dc);
                const div = document.createElement('div');
                const colorValue = 255 - Math.min(255, Math.abs(normalized) * 3);
                div.style.backgroundColor = `rgb(${colorValue}, ${colorValue}, ${colorValue})`;
                canvasStep3.appendChild(div);
            });
        }

        function getStandardQuantizationMatrix(quality) {
            // 标准量化矩阵
            const Q50 = [
                [16,11,10,16,24,40,51,61],
                [12,12,14,19,26,58,60,55],
                [14,13,16,24,40,57,69,56],
                [14,17,22,29,51,87,80,62],
                [18,22,37,56,68,109,103,77],
                [24,35,55,64,81,104,113,92],
                [49,64,78,87,103,121,120,101],
                [72,92,95,98,112,100,103,99]
            ];
            let scale;
            if (quality < 50) {
                scale = 5000 / quality;
            } else {
                scale = 200 - quality * 2;
            }
            const Q = Q50.map(row => row.map(val => Math.floor((val * scale + 50) / 100)));
            // 避免量化因子为0
            for (let i = 0; i < Q.length; i++) {
                for (let j = 0; j < Q[i].length; j++) {
                    Q[i][j] = Q[i][j] < 1 ? 1 : Q[i][j];
                }
            }
            return Q;
        }
    </script>
</body>
</html>


这段代码的缺点是，第三第四个步骤的可视化没有能正常地显示，大多数时候只能看到一大堆黑块而且跟原图的比例不匹配，根本不知道是什么意思。而且我注意到哈夫曼编码的符号出现了NaN；再者，我希望能用canvas画出量化矩阵，并支持让用户调整量化矩阵，显示颜色空间转换的数值过程，支持让用户调整哈夫曼编码数量的多少，从而反馈在最终的压缩结果。
此外你需要再思考怎样让这个可视化变得更直观更有趣更寓教于乐，并相应地去改进这个程序。