基于JavaScript+HTML實(shí)現(xiàn)文章逐句高亮朗讀功能
效果演示
項(xiàng)目核心
本項(xiàng)目主要包含以下核心功能:
- 語(yǔ)音合成(Text-to-Speech)功能
- 控制播放、暫停、繼續(xù)和停止操作
- 語(yǔ)音選擇功能
- 閱讀進(jìn)度保存與恢復(fù)
- 句子級(jí)高亮顯示
- 點(diǎn)擊任意句子直接跳轉(zhuǎn)并朗讀
頁(yè)面結(jié)構(gòu)
控制區(qū)域
包含所有操作按鈕(開(kāi)始、暫停、繼續(xù)、停止、重置)和語(yǔ)音選擇下拉框。
<div class="controls"> <button id="playBtn">開(kāi)始朗讀</button> <button id="pauseBtn" disabled>暫停</button> <button id="resumeBtn" disabled>繼續(xù)</button> <button id="stopBtn" disabled>停止</button> <select id="voiceSelect" class="voice-select"></select> <button id="resetBtn">重置進(jìn)度</button> </div>
文章區(qū)域
包含多個(gè)段落,每個(gè)段落由多個(gè)可交互的句子組成。
<div class="article" id="article"> <p class="paragraph"> <span class="sentence">在編程的世界里,學(xué)習(xí)是一個(gè)永無(wú)止境的過(guò)程。</span> <span class="sentence">隨著技術(shù)的不斷發(fā)展,我們需要不斷更新自己的知識(shí)和技能。</span> <span class="sentence">HTML、CSS和JavaScript是構(gòu)建現(xiàn)代網(wǎng)頁(yè)的三大基石。</span> </p> <p class="paragraph"> <span class="sentence">掌握這些基礎(chǔ)技術(shù)后,你可以進(jìn)一步學(xué)習(xí)各種前端框架和工具。</span> <span class="sentence">React、Vue和Angular是目前最流行的前端框架。</span> <span class="sentence">它們都采用了組件化的開(kāi)發(fā)模式,提高了代碼的可維護(hù)性和復(fù)用性。</span> </p> <p class="paragraph"> <span class="sentence">除了前端技術(shù),后端開(kāi)發(fā)也是全棧工程師必須掌握的技能。</span> <span class="sentence">Node.js讓JavaScript可以用于服務(wù)器端編程,大大擴(kuò)展了JavaScript的應(yīng)用范圍。</span> <span class="sentence">數(shù)據(jù)庫(kù)技術(shù)也是開(kāi)發(fā)中的重要組成部分。</span> </p> </div>
進(jìn)度信息
顯示當(dāng)前閱讀進(jìn)度。
<div class="progress-info"> 當(dāng)前進(jìn)度: <span id="progressText">0/0</span> <div class="progress-bar-container"> <div class="progress-bar"></div> </div> </div>
核心功能實(shí)現(xiàn)
定義基礎(chǔ)變量
獲取DOM元素
const sentences = document.querySelectorAll('.sentence'); const playBtn = document.getElementById('playBtn'); const pauseBtn = document.getElementById('pauseBtn'); const resumeBtn = document.getElementById('resumeBtn'); const stopBtn = document.getElementById('stopBtn'); const resetBtn = document.getElementById('resetBtn'); const voiceSelect = document.getElementById('voiceSelect'); const progressText = document.getElementById('progressText'); const progressBar = document.querySelector('.progress-bar');
定義語(yǔ)音合成相關(guān)變量
let speechSynthesis = window.speechSynthesis; let voices = []; let currentUtterance = null; let currentSentenceIndex = 0; let isPaused = false;
語(yǔ)音合成初始化
通過(guò) window.speechSynthesis API 獲取系統(tǒng)支持的語(yǔ)音列表,并填充到下拉選擇框中。
function initSpeechSynthesis() { // 獲取可用的語(yǔ)音列表 voices = speechSynthesis.getVoices(); // 填充語(yǔ)音選擇下拉框 voiceSelect.innerHTML = ''; voices.forEach((voice, index) => { const option = document.createElement('option'); option.value = index; option.textContent = `${voice.name} (${voice.lang})`; voiceSelect.appendChild(option); }); // 嘗試選擇中文語(yǔ)音 const chineseVoice = voices.find(voice =>{ voice.lang.includes('zh') || voice.lang.includes('cmn') }); if (chineseVoice) { const voiceIndex = voices.indexOf(chineseVoice); voiceSelect.value = voiceIndex; } }
句子朗讀功能
function speakSentence(index) { if (index >= sentences.length || index < 0) return; // 停止當(dāng)前朗讀 if (currentUtterance) { speechSynthesis.cancel(); } // 更新當(dāng)前句子高亮 updateHighlight(index); // 創(chuàng)建新的語(yǔ)音合成實(shí)例 const selectedVoiceIndex = voiceSelect.value; const utterance = new SpeechSynthesisUtterance(sentences[index].textContent); if (voices[selectedVoiceIndex]) { utterance.voice = voices[selectedVoiceIndex]; } utterance.rate = 0.9; // 稍微慢一點(diǎn)的語(yǔ)速 // 朗讀開(kāi)始時(shí)的處理 utterance.onstart = function() { sentences[index].classList.add('reading'); playBtn.disabled = true; pauseBtn.disabled = false; resumeBtn.disabled = true; stopBtn.disabled = false; }; // 朗讀結(jié)束時(shí)的處理 utterance.onend = function() { sentences[index].classList.remove('reading'); if (!isPaused) { if (currentSentenceIndex >= sentences.length - 1) { // 朗讀完成 playBtn.disabled = false; pauseBtn.disabled = true; resumeBtn.disabled = true; stopBtn.disabled = true; updateProgressText(); return; } currentSentenceIndex++; saveProgress(); speakSentence(currentSentenceIndex); } }; // 開(kāi)始朗讀 currentUtterance = utterance; speechSynthesis.speak(utterance); updateProgressText(); }
句子高亮功能
function updateHighlight(index) { sentences.forEach((sentence, i) => { sentence.classList.remove('current'); if (i === index) { sentence.classList.add('current'); // 滾動(dòng)到當(dāng)前句子 sentence.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }); }
更新進(jìn)度文本
function updateProgressText() { progressText.textContent = `${currentSentenceIndex + 1}/${sentences.length}`; const percentage = (currentSentenceIndex + 1) / sentences.length * 100; progressBar.style.width = `${percentage}%`; }
進(jìn)度保存與恢復(fù)
保存進(jìn)度到本地存儲(chǔ)
function saveProgress() { localStorage.setItem('readingProgress', currentSentenceIndex); localStorage.setItem('articleId', 'demoArticle'); updateProgressText(); }
從本地存儲(chǔ)加載進(jìn)度
function loadProgress() { const savedArticleId = localStorage.getItem('articleId'); if (savedArticleId === 'demoArticle') { const savedProgress = localStorage.getItem('readingProgress'); if (savedProgress !== null) { currentSentenceIndex = parseInt(savedProgress); if (currentSentenceIndex >= sentences.length) { currentSentenceIndex = 0; } updateHighlight(currentSentenceIndex); updateProgressText(); } } }
點(diǎn)擊句子朗讀跳轉(zhuǎn)功能
sentences.forEach((sentence, index) => { sentence.addEventListener('click', function() { currentSentenceIndex = index; speakSentence(currentSentenceIndex); }); });
擴(kuò)展建議
- 語(yǔ)速調(diào)節(jié):增加語(yǔ)速調(diào)節(jié)滑塊,讓用戶自定義朗讀速
- 多語(yǔ)言支持:自動(dòng)檢測(cè)文本語(yǔ)言并選擇合適的語(yǔ)音引擎
- 斷句優(yōu)化:改進(jìn)自然語(yǔ)言處理邏輯,使朗讀更符合口語(yǔ)習(xí)慣
- 多文章支持:擴(kuò)展文章管理系統(tǒng),允許用戶選擇不同文章進(jìn)行朗讀
完整代碼
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>文章逐句高亮朗讀</title> <style> body { font-family: 'Microsoft YaHei', sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 40px 20px; color: #333; height: 100vh; box-sizing: border-box; background: linear-gradient(to bottom right, #f8f9fa, #e9ecef); } h1 { text-align: center; color: #2c3e50; margin-bottom: 40px; font-size: 2.5em; letter-spacing: 2px; position: relative; animation: fadeInDown 1s ease-out forwards; } @keyframes fadeInDown { from { opacity: 0; transform: translateY(-30px); } to { opacity: 1; transform: translateY(0); } } h1::after { content: ''; display: block; width: 100px; height: 4px; background: linear-gradient(to right, #3498db, #2980b9); margin: 15px auto 0; border-radius: 2px; animation: growLine 1s ease-out forwards; } @keyframes growLine { from { width: 0; } to { width: 100px; } } .controls { box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); border-radius: 10px; padding: 20px; background-color: #ffffffcc; display: flex; flex-direction: column; gap: 15px; margin-bottom: 30px; } .controls > div { display: flex; flex-wrap: wrap; justify-content: center; gap: 15px; } button { padding: 10px 20px; background: linear-gradient(135deg, #3498db, #2980b9); color: white; border: none; border-radius: 25px; cursor: pointer; font-size: 16px; transition: all 0.3s ease-in-out; box-shadow: 0 4px 6px rgba(52, 152, 219, 0.3); } button:hover { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(52, 152, 219, 0.4); } button:disabled { background: linear-gradient(135deg, #95a5a6, #7f8c8d); box-shadow: none; transform: none; } .article { font-size: 18px; line-height: 1.8; background-color: #ffffffee; border-radius: 10px; padding: 25px; margin-top: 30px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05); margin-bottom: 30px; position: relative; z-index: 0 } .article::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(circle at top left, rgba(52, 152, 219, 0.05) 0%, transparent 100%); z-index: -1; border-radius: 10px; } .paragraph { margin-bottom: 20px; } .sentence { border-radius: 3px; transition: all 0.3s ease-in-out; cursor: pointer; position: relative; z-index: 1; } .sentence:hover { background-color: #f0f0f0; } .sentence::after { content: ''; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(255, 255, 255, 0.3); opacity: 0; z-index: -1; transition: opacity 0.3s ease-in-out; } .sentence:hover::after { opacity: 1; } .current { background-color: #fffde7 !important; font-weight: bold; transform: scale(1.05); box-shadow: 0 2px 8px rgba(255, 221, 0, 0.3); } .progress-info { text-align: center; margin-top: 20px; font-size: 14px; color: #7f8c8d; } select { padding: 8px; border-radius: 4px; border: 1px solid #bdc3c7; font-size: 16px; } .voice-select { min-width: 220px; padding: 10px 12px; border-radius: 25px; border: 1px solid #bdc3c7; font-size: 16px; background-color: #f8f9fa; transition: all 0.3s ease-in-out; appearance: none; background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'%3E%3Cpath fill='%23555' d='M7 10l5 5 5-5z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 15px center; background-size: 12px; display: block; margin: 0 auto; } .voice-select:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); } .progress-info { text-align: center; margin-top: 30px; font-size: 14px; color: #7f8c8d; position: relative; height: 30px; } .progress-bar-container { width: 100%; height: 6px; background-color: #ecf0f1; border-radius: 3px; overflow: hidden; margin: 10px 0; } .progress-bar { height: 100%; width: 0; background: linear-gradient(to right, #3498db, #2980b9); transition: width 0.3s ease-in-out; } </style> </head> <body> <h1>文章逐句高亮朗讀</h1> <div class="controls"> <div> <button id="playBtn">開(kāi)始朗讀</button> <button id="pauseBtn" disabled>暫停</button> <button id="resumeBtn" disabled>繼續(xù)</button> <button id="stopBtn" disabled>停止</button> <button id="resetBtn">重置進(jìn)度</button> </div> <select id="voiceSelect" class="voice-select"></select> </div> <div class="article" id="article"> <p class="paragraph"> <span class="sentence">在編程的世界里,學(xué)習(xí)是一個(gè)永無(wú)止境的過(guò)程。</span> <span class="sentence">隨著技術(shù)的不斷發(fā)展,我們需要不斷更新自己的知識(shí)和技能。</span> <span class="sentence">HTML、CSS和JavaScript是構(gòu)建現(xiàn)代網(wǎng)頁(yè)的三大基石。</span> </p> <p class="paragraph"> <span class="sentence">掌握這些基礎(chǔ)技術(shù)后,你可以進(jìn)一步學(xué)習(xí)各種前端框架和工具。</span> <span class="sentence">React、Vue和Angular是目前最流行的前端框架。</span> <span class="sentence">它們都采用了組件化的開(kāi)發(fā)模式,提高了代碼的可維護(hù)性和復(fù)用性。</span> </p> <p class="paragraph"> <span class="sentence">除了前端技術(shù),后端開(kāi)發(fā)也是全棧工程師必須掌握的技能。</span> <span class="sentence">Node.js讓JavaScript可以用于服務(wù)器端編程,大大擴(kuò)展了JavaScript的應(yīng)用范圍。</span> <span class="sentence">數(shù)據(jù)庫(kù)技術(shù)也是開(kāi)發(fā)中的重要組成部分。</span> </p> </div> <div class="progress-info"> 當(dāng)前進(jìn)度: <span id="progressText">0/0</span> <div class="progress-bar-container"> <div class="progress-bar"></div> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { // 獲取DOM元素 const sentences = document.querySelectorAll('.sentence'); const playBtn = document.getElementById('playBtn'); const pauseBtn = document.getElementById('pauseBtn'); const resumeBtn = document.getElementById('resumeBtn'); const stopBtn = document.getElementById('stopBtn'); const resetBtn = document.getElementById('resetBtn'); const voiceSelect = document.getElementById('voiceSelect'); const progressText = document.getElementById('progressText'); const progressBar = document.querySelector('.progress-bar'); // 語(yǔ)音合成相關(guān)變量 let speechSynthesis = window.speechSynthesis; let voices = []; let currentUtterance = null; let currentSentenceIndex = 0; let isPaused = false; // 從本地存儲(chǔ)加載進(jìn)度 loadProgress(); // 初始化語(yǔ)音合成 function initSpeechSynthesis() { // 獲取可用的語(yǔ)音列表 voices = speechSynthesis.getVoices(); // 填充語(yǔ)音選擇下拉框 voiceSelect.innerHTML = ''; voices.forEach((voice, index) => { const option = document.createElement('option'); option.value = index; option.textContent = `${voice.name} (${voice.lang})`; voiceSelect.appendChild(option); }); // 嘗試選擇中文語(yǔ)音 const chineseVoice = voices.find(voice =>{ voice.lang.includes('zh') || voice.lang.includes('cmn') }); if (chineseVoice) { const voiceIndex = voices.indexOf(chineseVoice); voiceSelect.value = voiceIndex; } } // 語(yǔ)音列表加載可能需要時(shí)間 speechSynthesis.onvoiceschanged = initSpeechSynthesis; initSpeechSynthesis(); // 朗讀指定句子 function speakSentence(index) { if (index >= sentences.length || index < 0) return; // 停止當(dāng)前朗讀 if (currentUtterance) { speechSynthesis.cancel(); } // 更新當(dāng)前句子高亮 updateHighlight(index); // 創(chuàng)建新的語(yǔ)音合成實(shí)例 const selectedVoiceIndex = voiceSelect.value; const utterance = new SpeechSynthesisUtterance(sentences[index].textContent); if (voices[selectedVoiceIndex]) { utterance.voice = voices[selectedVoiceIndex]; } utterance.rate = 0.9; // 稍微慢一點(diǎn)的語(yǔ)速 // 朗讀開(kāi)始時(shí)的處理 utterance.onstart = function() { sentences[index].classList.add('reading'); playBtn.disabled = true; pauseBtn.disabled = false; resumeBtn.disabled = true; stopBtn.disabled = false; }; // 朗讀結(jié)束時(shí)的處理 utterance.onend = function() { sentences[index].classList.remove('reading'); if (!isPaused) { if (currentSentenceIndex >= sentences.length - 1) { // 朗讀完成 playBtn.disabled = false; pauseBtn.disabled = true; resumeBtn.disabled = true; stopBtn.disabled = true; updateProgressText(); return; } currentSentenceIndex++; saveProgress(); speakSentence(currentSentenceIndex); } }; // 開(kāi)始朗讀 currentUtterance = utterance; speechSynthesis.speak(utterance); updateProgressText(); } // 更新句子高亮 function updateHighlight(index) { sentences.forEach((sentence, i) => { sentence.classList.remove('current'); if (i === index) { sentence.classList.add('current'); // 滾動(dòng)到當(dāng)前句子 sentence.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }); } // 更新進(jìn)度文本 function updateProgressText() { progressText.textContent = `${currentSentenceIndex + 1}/${sentences.length}`; const percentage = (currentSentenceIndex + 1) / sentences.length * 100; progressBar.style.width = `${percentage}%`; } // 保存進(jìn)度到本地存儲(chǔ) function saveProgress() { localStorage.setItem('readingProgress', currentSentenceIndex); localStorage.setItem('articleId', 'demoArticle'); // 在實(shí)際應(yīng)用中可以使用文章ID updateProgressText(); } // 從本地存儲(chǔ)加載進(jìn)度 function loadProgress() { const savedArticleId = localStorage.getItem('articleId'); if (savedArticleId === 'demoArticle') { const savedProgress = localStorage.getItem('readingProgress'); if (savedProgress !== null) { currentSentenceIndex = parseInt(savedProgress); if (currentSentenceIndex >= sentences.length) { currentSentenceIndex = 0; } updateHighlight(currentSentenceIndex); updateProgressText(); } } } // 事件監(jiān)聽(tīng)器 playBtn.addEventListener('click', function() { currentSentenceIndex = 0; speakSentence(currentSentenceIndex); }); pauseBtn.addEventListener('click', function() { if (speechSynthesis.speaking && !isPaused) { speechSynthesis.pause(); isPaused = true; pauseBtn.disabled = true; resumeBtn.disabled = false; } }); resumeBtn.addEventListener('click', function() { if (isPaused) { speechSynthesis.resume(); isPaused = false; pauseBtn.disabled = false; resumeBtn.disabled = true; } }); stopBtn.addEventListener('click', function() { speechSynthesis.cancel(); isPaused = false; playBtn.disabled = false; pauseBtn.disabled = true; resumeBtn.disabled = true; stopBtn.disabled = true; // 移除所有朗讀樣式 sentences.forEach(sentence => { sentence.classList.remove('reading'); }); }); resetBtn.addEventListener('click', function() { localStorage.removeItem('readingProgress'); localStorage.removeItem('articleId'); currentSentenceIndex = 0; updateHighlight(currentSentenceIndex); updateProgressText(); }); // 點(diǎn)擊句子跳轉(zhuǎn)到該句子并朗讀 sentences.forEach((sentence, index) => { sentence.addEventListener('click', function() { currentSentenceIndex = index; speakSentence(currentSentenceIndex); }); }); }); </script> </body> </html>
到此這篇關(guān)于基于JavaScript+HTML實(shí)現(xiàn)文章逐句高亮朗讀功能的文章就介紹到這了,更多相關(guān)JavaScript HTML文章高亮朗讀內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js改變img標(biāo)簽的src屬性在IE下沒(méi)反應(yīng)的解決方法
在Chrome FF里都能改變成功,但在IE下卻不行,網(wǎng)上搜了半天,大概了解了,這個(gè)是IE的一個(gè)bug,具體的解決方法如下,有類似問(wèn)題的朋友可以參考下哈,希望對(duì)大家有所幫助2013-07-07layui-tree實(shí)現(xiàn)Ajax異步請(qǐng)求后動(dòng)態(tài)添加節(jié)點(diǎn)的方法
今天小編就為大家分享一篇layui-tree實(shí)現(xiàn)Ajax異步請(qǐng)求后動(dòng)態(tài)添加節(jié)點(diǎn)的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-09-09使用canvas實(shí)現(xiàn)鯉魚(yú)躍龍門的動(dòng)畫(huà)效果
這篇文章主要給大家介紹了使用canvas實(shí)現(xiàn)鯉魚(yú)躍龍門的動(dòng)畫(huà)效果,文中通過(guò)代碼示例給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,感興趣的小伙伴可以自己動(dòng)手嘗試一下2024-02-02小程序?qū)崿F(xiàn)列表多個(gè)批量倒計(jì)時(shí)
這篇文章主要為大家詳細(xì)介紹了小程序?qū)崿F(xiàn)列表多個(gè)批量倒計(jì)時(shí),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02Js遍歷鍵值對(duì)形式對(duì)象或Map形式的方法
下面小編就為大家?guī)?lái)一篇Js遍歷鍵值對(duì)形式對(duì)象或Map形式的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-08-08IE 下Enter提交表單存在重復(fù)提交問(wèn)題的解決方法
這篇文章主要介紹了IE 下Enter提交表單存在重復(fù)提交問(wèn)題的解決方法,需要的朋友可以參考下2014-05-05JS實(shí)現(xiàn)點(diǎn)擊Radio動(dòng)態(tài)更新table數(shù)據(jù)
這篇文章主要介紹了JS實(shí)現(xiàn)點(diǎn)擊Radio動(dòng)態(tài)更新table數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下2017-07-07