The integrated accessibility widget is a comprehensive client-side tool designed to adapt digital environments to the diverse needs of users. Operating directly within the web browser, it allows individuals to dynamically customise text presentation, alter visual contrast profiles, and activate interactive visual aids without requiring external software installations.
By providing real-time, user-triggered modifications over presentation layers, the widget helps eliminate systemic digital barriers, transforming static web pages into fluid, adaptable user interfaces tailored to individual comfort and functional necessity.
The primary target audience encompasses individuals with visual impairments, including varying degrees of low vision, colour blindness, and age-related vision deterioration.
Through the magnification tools, users can scale typography independently of the global browser zoom, preventing layout breakages while preserving readability.
The high contrast and greyscale modes provide alternative colour mapping schemas to optimise text-to-background contrast ratios, making text discernable for those struggling with standard colour configurations or experiencing severe light sensitivity.
Additionally, the widget is specifically engineered to assist neurodivergent individuals and those experiencing cognitive or learning difficulties, such as dyslexia or attention deficit disorders.
The "Dyslexia Font" option alters typography and maximises line and word spacing to prevent text crowding and reduce reading fatigue.
For users who struggle to maintain visual track across extensive, dense passages, the "Reading Guide" introduces a cursor-aligned horizontal ruler that isolates blocks of text, mimicking physical reading overlay filters to boost comprehension and focus.
Finally, the widget introduces multi-modal interaction channels through its integrated text-to-speech (TTS) and visual focus pulse mechanisms, which are highly beneficial for individuals with auditory learning preferences, severe vision loss, or situational reading barriers.
The automated screen narration converts textual data into synthesised speech, while the synchronised visual highlight maps out individual words on-screen.
Simultaneously, the focus pulse reader physically scales and illuminates the precise word being consumed, creating an optimal environment for literacy advancement, situational multi-tasking, or reading in cognitively demanding scenarios.
Accessibility Widget
<style>
/* =========================================
COMPONENT RESET & ISOLATION
========================================= */
#a11y-widget-container,
#a11y-widget-container * {
font-size: 14px !important;
line-height: normal !important;
letter-spacing: normal !important;
word-spacing: normal !important;
text-transform: none !important;
box-sizing: border-box !important;
}
#a11y-widget-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 2147483647 !important;
font-family: Arial, sans-serif !important;
}
/* =========================================
ACCESSIBILITY PANEL STYLES
========================================= */
#a11y-toggle-btn {
background-color: #333 !important;
color: white !important;
border: none !important;
padding: 12px 18px !important;
border-radius: 50px !important;
cursor: pointer !important;
font-size: 16px !important;
font-weight: bold !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.2) !important;
display: inline-flex !important;
align-items: center !important;
gap: 8px !important;
}
#a11y-toggle-btn svg {
width: 20px !important;
height: 20px !important;
fill: currentColor !important;
}
#a11y-panel {
display: none;
position: relative !important;
background-color: #f9f9f9 !important;
border: 1px solid #ccc !important;
padding: 15px !important;
border-radius: 8px !important;
margin-bottom: 10px !important;
box-shadow: 0 4px 8px rgba(0,0,0,0.2) !important;
width: 240px !important;
}
#seanduffy-auth-link:hover {
color: #333 !important;
}
#a11y-panel button.a11y-tool-btn {
display: flex !important;
align-items: center !important;
gap: 10px !important;
width: 100% !important;
margin-bottom: 8px !important;
padding: 10px !important;
background-color: #e0e0e0 !important;
color: #333 !important;
border: 1px solid #bbb !important;
border-radius: 4px !important;
cursor: pointer !important;
text-align: left !important;
font-weight: normal !important;
}
#a11y-panel button.a11y-tool-btn svg {
width: 18px !important;
height: 18px !important;
fill: currentColor !important;
flex-shrink: 0 !important;
}
#a11y-panel button.a11y-tool-btn:hover {
background-color: #d0d0d0 !important;
}
#a11y-panel button.a11y-close-btn {
position: absolute !important;
top: 12px !important;
right: 12px !important;
background: transparent !important;
border: none !important;
color: #666 !important;
cursor: pointer !important;
padding: 2px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: auto !important;
margin: 0 !important;
}
#a11y-panel button.a11y-close-btn:hover {
color: #000 !important;
}
#a11y-panel button.a11y-close-btn svg {
width: 18px !important;
height: 18px !important;
fill: currentColor !important;
}
#a11y-panel button.a11y-reset-btn {
background-color: #d9534f !important;
color: white !important;
border: 1px solid #d43f3a !important;
font-weight: bold !important;
justify-content: center !important;
margin-top: 15px !important;
margin-bottom: 0px !important;
}
#a11y-panel button.a11y-reset-btn:hover {
background-color: #c9302c !important;
}
/* Reading Ruler Line */
#a11y-ruler {
display: none !important;
position: fixed !important;
top: 30%;
left: 0;
width: 100% !important;
height: 4px !important;
background-color: #0056b3 !important;
z-index: 2147483646 !important;
pointer-events: none !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.4) !important;
}
#a11y-ruler.a11y-ruler-active {
display: block !important;
}
/* Custom Rule Modifiers */
body.a11y-highlight-links a {
background-color: #ffff00 !important;
color: #000000 !important;
text-decoration: underline !important;
font-weight: bold !important;
padding: 2px !important;
}
body.a11y-dyslexia,
body.a11y-dyslexia p,
body.a11y-dyslexia span,
body.a11y-dyslexia li {
font-family: Arial, Helvetica, sans-serif !important;
line-height: 2.0 !important;
letter-spacing: 0.08em !important;
word-spacing: 0.15em !important;
}
html.a11y-monochrome {
filter: grayscale(100%) !important;
}
html.a11y-high-contrast {
filter: invert(1) hue-rotate(180deg) !important;
}
html.a11y-monochrome.a11y-high-contrast {
filter: grayscale(100%) invert(1) hue-rotate(180deg) !important;
}
/* Protect Widget and Media components from inversion filters */
html.a11y-high-contrast img,
html.a11y-high-contrast video,
html.a11y-high-contrast #a11y-widget-container,
html.a11y-high-contrast #a11y-ruler {
filter: invert(1) hue-rotate(180deg) !important;
}
html.a11y-monochrome.a11y-high-contrast img,
html.a11y-monochrome.a11y-high-contrast video,
html.a11y-monochrome.a11y-high-contrast #a11y-widget-container {
filter: invert(1) hue-rotate(180deg) grayscale(100%) !important;
}
/* =========================================
TEXT-TO-SPEECH (TTS) FLOATING CONTROL BAR
========================================= */
.tts-active-word {
background-color: #000000 !important;
color: #ffffff !important;
border-radius: 3px;
padding: 2px 0;
box-shadow: 0 0 0 2px #000000;
transition: background-color 0.1s, color 0.1s;
}
#tts-control-bar, #focus-pulse-control-bar {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%) translateY(150px);
z-index: 100000;
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
#tts-control-bar.tts-visible, #focus-pulse-control-bar.pulse-visible {
transform: translateX(-50%) translateY(0);
}
.tts-pill {
background-color: #1a1a1a;
border: 1px solid #333;
border-radius: 50px;
padding: 8px 24px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05);
min-width: max-content !important; /* Force width to accommodate children */
flex-wrap: nowrap !important; /* Prevent internal wrapping */
}
.tts-controls {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0 !important;
}
.tts-icon-btn {
background: none;
border: none;
color: #999999;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: color 0.2s, transform 0.1s, background-color 0.2s;
flex-shrink: 0 !important;
}
.tts-icon-btn:hover:not(:disabled) {
color: #ffffff;
transform: scale(1.05);
}
.tts-icon-btn:active:not(:disabled) {
transform: scale(0.95);
}
.tts-icon-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.tts-icon-btn.tts-primary {
color: #007bff;
}
.tts-icon-btn.tts-primary:hover:not(:disabled) {
color: #3395ff;
}
.tts-divider {
width: 1px;
height: 24px;
background-color: #333333;
flex-shrink: 0 !important;
}
.tts-speed-control {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0 !important;
}
.tts-speed-control span {
white-space: nowrap !important;
}
#tts-speed-input {
cursor: pointer;
accent-color: #007bff;
width: 80px;
flex-shrink: 0 !important;
}
#tts-speed-display {
color: #cccccc;
font-size: 13px;
font-weight: 600;
min-width: 32px;
}
/* =========================================
FOCUS PULSE VISUAL ENGINE STYLES
========================================= */
.reader-word {
display: inline-block;
transition: transform 0.15s ease, color 0.15s ease, text-shadow 0.15s ease;
}
.reader-enlarged {
transform: scale(1.2) !important;
color: #e74c3c !important;
font-weight: bold !important;
z-index: 10 !important;
position: relative !important;
text-shadow: 0px 4px 12px rgba(231, 76, 60, 0.6) !important;
}
#reader-speed {
-webkit-appearance: none;
appearance: none;
width: 90px;
height: 6px;
background: #444;
border-radius: 3px;
outline: none;
cursor: pointer;
flex-shrink: 0 !important;
}
#reader-speed::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: #0073aa;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
#reader-speed::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: #0073aa;
border: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.pulse-btn-text {
background: #0073aa !important;
color: white !important;
border: none !important;
padding: 6px 16px !important;
border-radius: 4px !important;
font-weight: bold !important;
cursor: pointer !important;
white-space: nowrap !important; /* Prevents text stacking */
flex-shrink: 0 !important;
}
.pulse-btn-text:hover {
background: #005177 !important;
}
@media (max-width: 480px) {
.tts-pill {
padding: 8px 12px;
gap: 10px;
}
#tts-speed-input, #reader-speed {
width: 50px;
}
.pulse-btn-text {
padding: 6px 12px !important;
}
}
</style>
<div id="a11y-ruler"></div>
<!-- Protected container hidden from system readers and traversal algorithms -->
<div id="a11y-widget-container" aria-hidden="true" class="exclude-tts">
<!-- Floating TTS Control Bar -->
<div id="tts-control-bar">
<div class="tts-pill">
<div class="tts-controls">
<button id="tts-play-btn" class="tts-icon-btn tts-primary" title="Play">
<svg viewbox="0 0 24 24" width="28" height="28" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
</button>
<button id="tts-pause-btn" class="tts-icon-btn" title="Pause" disabled>
<svg viewbox="0 0 24 24" width="28" height="28" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
</button>
<button id="tts-stop-btn" class="tts-icon-btn" title="Stop" disabled>
<svg viewbox="0 0 24 24" width="28" height="28" fill="currentColor"><path d="M6 6h12v12H6z"/></svg>
</button>
</div>
<div class="tts-divider"></div>
<div class="tts-speed-control" title="Adjust Speed">
<input type="range" id="tts-speed-input" min="0.5" max="1.5" step="0.1" value="1" />
<span id="tts-speed-display">1.0x</span>
</div>
<div class="tts-divider"></div>
<button id="tts-close" class="tts-icon-btn" title="Close Reader">
<svg viewbox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</div>
</div>
<!-- Floating Focus Pulse Control Bar -->
<div id="focus-pulse-control-bar">
<div class="tts-pill">
<div class="tts-controls">
<button id="reader-play-btn" class="pulse-btn-text">Play</button>
</div>
<div class="tts-divider"></div>
<div class="tts-speed-control" title="Adjust Timing Speed">
<span style="color: #999; font-size: 12px;">Speed</span>
<input type="range" id="reader-speed" min="50" max="600" value="250">
</div>
<div class="tts-divider"></div>
<button id="focus-pulse-close" class="tts-icon-btn" title="Close Focus Pulse">
<svg viewbox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</div>
</div>
<!-- Main Menu Panel -->
<div id="a11y-panel">
<button onclick="toggleWidget()" class="a11y-close-btn" title="Close Panel">
<svg viewbox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
<div style="text-align: left; font-weight: bold; margin-bottom: 5px; color: #333 !important;">Accessibility Tools</div>
<!-- Tamper-proof Branding Link -->
<a id="seanduffy-auth-link" href="https://www.seanduffy.uk" target="_blank" style="display: flex !important; align-items: center !important; gap: 6px !important; color: #888 !important; text-decoration: none !important; font-size: 13px !important; margin-bottom: 15px !important; padding-right: 25px !important; transition: color 0.2s !important;">
<svg viewbox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>
seanduffy.uk
</a>
<button id="tts-launcher" class="a11y-tool-btn">
<svg viewbox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>
Listen to Article
</button>
<button id="focus-pulse-launcher" class="a11y-tool-btn">
<svg viewbox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>
Focus Pulse
</button>
<button onclick="changeTextSize(1)" class="a11y-tool-btn">
<svg viewbox="0 0 24 24"><path d="M4 19h2.42l1.27-3.58h5.65l1.28 3.58H17L11.5 5h-3L4 19zm6.42-4.84L10 10.73l-.42 3.43h.84zM20 9h-2V7h-2v2h-2v2h2v2h2v-2h2V9z"/></path></svg>
Increase Text Size
</button>
<button onclick="changeTextSize(-1)" class="a11y-tool-btn">
<svg viewbox="0 0 24 24"><path d="M4 19h2.42l1.27-3.58h5.65l1.28 3.58H17L11.5 5h-3L4 19zm6.42-4.84L10 10.73l-.42 3.43h.84zM14 11h6V9h-6v2z"/></path></svg>
Decrease Text Size
</button>
<button onclick="toggleContrast()" class="a11y-tool-btn">
<svg viewbox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8v16z"/></path></svg>
High Contrast
</button>
<button onclick="toggleLinks()" class="a11y-tool-btn">
<svg viewbox="0 0 24 24"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></path></svg>
Highlight Links
</button>
<button onclick="toggleDyslexia()" class="a11y-tool-btn">
<svg viewbox="0 0 24 24"><path d="M21 5c-1.11-.64-2.58-1-4-1-2.05 0-4.14.81-5.5 2-1.36-1.19-3.45-2-5.5-2-1.42 0-2.89.36-4 1v14c1.11-.64 2.58-1 4-1 2.05 0 4.14.81 5.5 2 1.36-1.19 3.45-2 5.5-2 1.42 0 2.89.36 4 1V5zm-1 12c-1.11-.45-2.39-.66-3.5-.66-2 0-4 .6-5.5 1.47V7.15c1.45-.8 3.39-1.15 5.5-1.15 1.42 0 2.8.25 3.5.6v10.4z"/></path></svg>
Dyslexia Font
</button>
<button onclick="toggleMonochrome()" class="a11y-tool-btn">
<svg viewbox="0 0 24 24"><path d="M12 2C6.49 2 2 6.49 2 12s4.49 10 10 10c1.38 0 2.5-1.12 2.5-2.5 0-.61-.22-1.19-.59-1.64-.09-.1-.13-.24-.13-.36 0-.28.22-.5.5-.5h1.72c3.31 0 6-2.69 6-6 0-4.96-4.49-9-10-9zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 7 6.5 7 8 7.67 8 8.5 7.33 11 6.5 11zm3-4C8.67 7 8 6.33 8 5.5S8.67 4 9.5 4s1.5.67 1.5 1.5S10.33 7 9.5 7zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 4 14.5 4s1.5.67 1.5 1.5S15.33 7 14.5 7zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 8 17.5 8s1.5 0 1.5 1.5-.67 1.5-1.5 1.5z"/></path></svg>
Greyscale Mode
</button>
<button onclick="toggleRuler()" class="a11y-tool-btn">
<svg viewbox="0 0 24 24"><path d="M17 2H7c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 18H7v-3h3v-2H7v-3h5v-2H7V8h3V6H7V4h10v16z"/></path></svg>
Reading Guide
</button>
<button onclick="resetAllSettings()" class="a11y-tool-btn a11y-reset-btn">
<svg viewbox="0 0 24 24"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></path></svg>
Reset All
</button>
</div>
<button id="a11y-toggle-btn" onclick="toggleWidget()">
<svg viewbox="0 0 24 24"><path d="M12 2c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 7h-6v13h-2v-6h-2v6H9V9H3V7h18v2z"/></svg>
Accessibility
</button>
</div>
<script>
/* =========================================
TAMPER PROOFING & SECURITY CHECK
========================================= */
const verifyIntegrity = function() {
const authLink = document.getElementById('seanduffy-auth-link');
const isValid = authLink &&
authLink.getAttribute('href') === 'https://www.seanduffy.uk' &&
authLink.querySelector('svg');
if (!isValid) {
const widget = document.getElementById('a11y-widget-container');
if (widget) widget.remove();
return false;
}
return true;
};
/* =========================================
ACCESSIBILITY PANEL CORE LOGIC
========================================= */
let currentScale = 1.0;
let sizesInitialised = false;
function toggleWidget() {
if (!verifyIntegrity()) return;
const panel = document.getElementById('a11y-panel');
panel.style.display = (panel.style.display === 'none' || panel.style.display === '') ? 'block' : 'none';
}
function initFontSizes() {
const mainContentSelectors = '.post-body, .post-body *, .entry-content, .entry-content *, article, article *';
const elements = document.querySelectorAll(mainContentSelectors);
elements.forEach(el => {
if (document.getElementById('a11y-widget-container').contains(el)) return;
if (!el.hasAttribute('data-orig-size')) {
const computedStyle = window.getComputedStyle(el);
const fontSize = computedStyle.fontSize;
if (fontSize && fontSize.includes('px')) {
el.setAttribute('data-orig-size', parseFloat(fontSize));
}
}
});
sizesInitialised = true;
}
function changeTextSize(direction) {
if (!verifyIntegrity() || !sizesInitialised) initFontSizes();
currentScale += direction * 0.15;
if (currentScale < 0.7) currentScale = 0.7;
if (currentScale > 2.0) currentScale = 2.0;
applyFontScale();
localStorage.setItem('a11y-font-scale', currentScale);
}
function applyFontScale() {
if (!sizesInitialised) initFontSizes();
const elements = document.querySelectorAll('[data-orig-size]');
elements.forEach(el => {
const originalSize = parseFloat(el.getAttribute('data-orig-size'));
el.style.setProperty('font-size', (originalSize * currentScale) + 'px', 'important');
});
}
function toggleContrast() {
if (!verifyIntegrity()) return;
const isActive = document.documentElement.classList.toggle('a11y-high-contrast');
localStorage.setItem('a11y-high-contrast', isActive ? 'true' : 'false');
}
function toggleLinks() {
if (!verifyIntegrity()) return;
const isActive = document.body.classList.toggle('a11y-highlight-links');
localStorage.setItem('a11y-highlight-links', isActive ? 'true' : 'false');
}
function toggleDyslexia() {
if (!verifyIntegrity()) return;
const isActive = document.body.classList.toggle('a11y-dyslexia');
localStorage.setItem('a11y-dyslexia', isActive ? 'true' : 'false');
}
function toggleMonochrome() {
if (!verifyIntegrity()) return;
const isActive = document.documentElement.classList.toggle('a11y-monochrome');
localStorage.setItem('a11y-monochrome', isActive ? 'true' : 'false');
}
function toggleRuler() {
if (!verifyIntegrity()) return;
const ruler = document.getElementById('a11y-ruler');
const isActive = ruler.classList.toggle('a11y-ruler-active');
if (isActive) {
window.addEventListener('mousemove', moveRuler);
localStorage.setItem('a11y-ruler', 'true');
} else {
window.removeEventListener('mousemove', moveRuler);
ruler.style.removeProperty('top');
localStorage.setItem('a11y-ruler', 'false');
}
}
function moveRuler(e) {
const ruler = document.getElementById('a11y-ruler');
ruler.style.top = e.clientY + 'px';
}
function resetAllSettings() {
if (!verifyIntegrity()) return;
localStorage.removeItem('a11y-font-scale');
localStorage.removeItem('a11y-high-contrast');
localStorage.removeItem('a11y-highlight-links');
localStorage.removeItem('a11y-dyslexia');
localStorage.removeItem('a11y-monochrome');
localStorage.removeItem('a11y-ruler');
currentScale = 1.0;
const elements = document.querySelectorAll('[data-orig-size]');
elements.forEach(el => {
el.style.removeProperty('font-size');
});
document.documentElement.classList.remove('a11y-high-contrast', 'a11y-monochrome');
document.body.classList.remove('a11y-highlight-links', 'a11y-dyslexia');
const ruler = document.getElementById('a11y-ruler');
ruler.classList.remove('a11y-ruler-active');
ruler.style.removeProperty('top');
window.removeEventListener('mousemove', moveRuler);
if (typeof window.stopFocusPulse === 'function') {
window.stopFocusPulse();
}
}
function loadSavedPreferences() {
if (!verifyIntegrity()) return;
if (localStorage.getItem('a11y-high-contrast') === 'true') {
document.documentElement.classList.add('a11y-high-contrast');
}
if (localStorage.getItem('a11y-monochrome') === 'true') {
document.documentElement.classList.add('a11y-monochrome');
}
if (localStorage.getItem('a11y-highlight-links') === 'true') {
document.body.classList.add('a11y-highlight-links');
}
if (localStorage.getItem('a11y-dyslexia') === 'true') {
document.body.classList.add('a11y-dyslexia');
}
if (localStorage.getItem('a11y-ruler') === 'true') {
const ruler = document.getElementById('a11y-ruler');
ruler.classList.add('a11y-ruler-active');
window.addEventListener('mousemove', moveRuler);
}
const savedScale = localStorage.getItem('a11y-font-scale');
if (savedScale) {
currentScale = parseFloat(savedScale);
setTimeout(applyFontScale, 100);
}
}
window.addEventListener('DOMContentLoaded', loadSavedPreferences);
/* =========================================
TEXT-TO-SPEECH (TTS) ENGINE LOGIC
========================================= */
(function() {
const CONTENT_SELECTORS = [
'.post-body', '.entry-content', 'article', '[itemprop="articleBody"]', '.main-content'
];
const launcher = document.getElementById('tts-launcher');
const controlBar = document.getElementById('tts-control-bar');
const closeBtn = document.getElementById('tts-close');
const playBtn = document.getElementById('tts-play-btn');
const pauseBtn = document.getElementById('tts-pause-btn');
const stopBtn = document.getElementById('tts-stop-btn');
const speedInput = document.getElementById('tts-speed-input');
const speedDisplay = document.getElementById('tts-speed-display');
if(!launcher) return;
const synth = window.speechSynthesis;
let isPlaying = false;
let isPaused = false;
let isCancelling = false;
let currentUtterance = null;
let targetContainer = null;
let sentencesData = [];
let currentSentenceIndex = 0;
let currentHighlightedSpan = null;
function updateUIState() {
if (isPlaying && !isPaused) {
playBtn.disabled = true;
pauseBtn.disabled = false;
stopBtn.disabled = false;
} else if (isPaused) {
playBtn.disabled = false;
pauseBtn.disabled = true;
stopBtn.disabled = false;
} else {
playBtn.disabled = false;
pauseBtn.disabled = true;
stopBtn.disabled = true;
}
}
function removeCurrentHighlight() {
if (currentHighlightedSpan) {
currentHighlightedSpan.classList.remove('tts-active-word');
currentHighlightedSpan = null;
}
}
function clearAllHighlights() {
removeCurrentHighlight();
if (targetContainer) {
const activeSpans = targetContainer.querySelectorAll('.tts-active-word');
activeSpans.forEach(span => span.classList.remove('tts-active-word'));
}
}
function prepareDOMForHighlighting() {
for (const selector of CONTENT_SELECTORS) {
const el = document.querySelector(selector);
if (el && el.innerText.trim().length > 150) {
targetContainer = el;
break;
}
}
if (!targetContainer) targetContainer = document.body;
if (targetContainer.dataset.ttsWrapped) return;
const walker = document.createTreeWalker(targetContainer, NodeFilter.SHOW_TEXT, {
acceptNode: function(node) {
const parent = node.parentElement;
if (!parent) return NodeFilter.FILTER_REJECT;
const excludedSelectors = 'script, style, noscript, #a11y-widget-container, button, input, textarea, #comments, .exclude-tts, .universal-code-box, pre, code, header, nav, footer, aside, .blog-description, .Header';
if (parent.closest(excludedSelectors)) {
return NodeFilter.FILTER_REJECT;
}
const style = window.getComputedStyle(parent);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return NodeFilter.FILTER_REJECT;
}
if (node.nodeValue.trim() === '') {
return NodeFilter.FILTER_SKIP;
}
return NodeFilter.FILTER_ACCEPT;
}
}, false);
const nodesToReplace = [];
let node;
while (node = walker.nextNode()) nodesToReplace.push(node);
nodesToReplace.forEach(textNode => {
const words = textNode.nodeValue.split(/(\s+)/);
const fragment = document.createDocumentFragment();
words.forEach(word => {
if (/\S/.test(word)) {
const span = document.createElement('span');
span.className = 'tts-trackable-word';
span.textContent = word;
fragment.appendChild(span);
} else {
fragment.appendChild(document.createTextNode(word));
}
});
textNode.parentNode.replaceChild(fragment, textNode);
});
targetContainer.dataset.ttsWrapped = 'true';
buildSentenceQueue();
}
function buildSentenceQueue() {
sentencesData = [];
if (!targetContainer) return;
const allSpans = Array.from(targetContainer.querySelectorAll('.tts-trackable-word'));
let currentSentenceText = "";
let currentSentenceSpans = [];
allSpans.forEach(span => {
currentSentenceSpans.push(span);
currentSentenceText += span.textContent + " ";
if (/[.!?]$/.test(span.textContent.trim())) {
sentencesData.push({ text: currentSentenceText, spans: currentSentenceSpans });
currentSentenceText = "";
currentSentenceSpans = [];
}
});
if (currentSentenceSpans.length > 0) {
sentencesData.push({ text: currentSentenceText, spans: currentSentenceSpans });
}
}
function readNextSentence() {
if (currentSentenceIndex >= sentencesData.length) {
stopReading();
return;
}
const sentenceData = sentencesData[currentSentenceIndex];
currentUtterance = new SpeechSynthesisUtterance(sentenceData.text);
currentUtterance.rate = parseFloat(speedInput.value);
currentUtterance.onboundary = function(event) {
if (event.name === 'word' || event.charIndex !== undefined) {
const charIndex = event.charIndex;
let runningLength = 0;
for (let i = 0; i < sentenceData.spans.length; i++) {
const span = sentenceData.spans[i];
const spanTextLength = span.textContent.length + 1;
if (charIndex >= runningLength && charIndex < runningLength + spanTextLength) {
removeCurrentHighlight();
span.classList.add('tts-active-word');
currentHighlightedSpan = span;
const rect = span.getBoundingClientRect();
const viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
if (rect.top < viewHeight * 0.25 || rect.bottom > viewHeight * 0.75) {
span.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
break;
}
runningLength += spanTextLength;
}
}
};
currentUtterance.onend = function() {
removeCurrentHighlight();
if (isPlaying && !isPaused && !isCancelling) {
currentSentenceIndex++;
readNextSentence();
}
};
currentUtterance.onerror = function(e) {
removeCurrentHighlight();
if (isPlaying && !isPaused && !isCancelling && e.error !== 'interrupted' && e.error !== 'canceled') {
currentSentenceIndex++;
readNextSentence();
}
};
synth.speak(currentUtterance);
}
function startReading() {
if (!verifyIntegrity()) return;
if (sentencesData.length === 0) {
prepareDOMForHighlighting();
if (sentencesData.length === 0) {
alert('Could not detect readable article text.');
return;
}
}
if (isPaused) {
synth.resume();
isPaused = false;
} else {
isCancelling = true;
synth.cancel();
isCancelling = false;
isPlaying = true;
readNextSentence();
}
updateUIState();
}
function pauseReading() {
synth.pause();
isPaused = true;
updateUIState();
}
function stopReading() {
isCancelling = true;
synth.cancel();
isCancelling = false;
isPlaying = false;
isPaused = false;
currentSentenceIndex = 0;
clearAllHighlights();
updateUIState();
}
launcher.addEventListener('click', () => {
if (!verifyIntegrity()) return;
if (typeof window.stopFocusPulse === 'function') window.stopFocusPulse();
controlBar.classList.add('tts-visible');
toggleWidget();
startReading();
});
closeBtn.addEventListener('click', () => {
stopReading();
controlBar.classList.remove('tts-visible');
});
playBtn.addEventListener('click', startReading);
pauseBtn.addEventListener('click', pauseReading);
stopBtn.addEventListener('click', stopReading);
speedInput.addEventListener('input', (e) => {
speedDisplay.textContent = parseFloat(e.target.value).toFixed(1) + 'x';
if (isPlaying && !isPaused) {
isCancelling = true;
synth.cancel();
isCancelling = false;
readNextSentence();
}
});
window.addEventListener('beforeunload', () => {
synth.cancel();
});
})();
/* =========================================
FOCUS PULSE ENGINE CORE LOGIC
========================================= */
document.addEventListener("DOMContentLoaded", function() {
if (!verifyIntegrity()) return;
const contentSelectors = '.entry-content, .post-body, article, .post-content';
const contentContainer = document.querySelector(contentSelectors);
if (!contentContainer) {
console.log("Focus Pulse: Content area not found.");
return;
}
let wordsArray = [];
function wrapWords(node) {
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
const textNodes = [];
while(walker.nextNode()) {
const parentElement = walker.currentNode.parentElement;
if (!parentElement) continue;
const parentTag = parentElement.tagName;
const isInsideWidget = parentElement.closest('#a11y-widget-container');
if(
walker.currentNode.nodeValue.trim() !== '' &&
parentTag !== 'SCRIPT' &&
parentTag !== 'STYLE' &&
!isInsideWidget
) {
textNodes.push(walker.currentNode);
}
}
textNodes.forEach(textNode => {
const textContent = textNode.nodeValue;
const parts = textContent.split(/(\s+)/);
const fragment = document.createDocumentFragment();
parts.forEach(part => {
if (part.trim().length > 0) {
const span = document.createElement('span');
span.className = 'reader-word';
span.textContent = part;
fragment.appendChild(span);
wordsArray.push(span);
} else {
fragment.appendChild(document.createTextNode(part));
}
});
textNode.parentNode.replaceChild(fragment, textNode);
});
}
wrapWords(contentContainer);
let currentIndex = 0;
let isPlaying = false;
let readerInterval;
const playBtn = document.getElementById('reader-play-btn');
const speedSlider = document.getElementById('reader-speed');
const pulseLauncher = document.getElementById('focus-pulse-launcher');
const pulseControlBar = document.getElementById('focus-pulse-control-bar');
const pulseCloseBtn = document.getElementById('focus-pulse-close');
function highlightNextWord() {
if (currentIndex > 0) {
wordsArray[currentIndex - 1].classList.remove('reader-enlarged');
} else if (currentIndex === 0 && wordsArray.length > 0) {
wordsArray[wordsArray.length - 1].classList.remove('reader-enlarged');
}
if (currentIndex >= wordsArray.length) {
stopReader();
currentIndex = 0;
return;
}
wordsArray[currentIndex].classList.add('reader-enlarged');
const rect = wordsArray[currentIndex].getBoundingClientRect();
if(rect.top > window.innerHeight - 300 || rect.top < 100) {
wordsArray[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
currentIndex++;
if (isPlaying) {
readerInterval = setTimeout(highlightNextWord, parseInt(speedSlider.value));
}
}
function startReader() {
if (!verifyIntegrity()) return;
if (wordsArray.length === 0) return;
isPlaying = true;
playBtn.textContent = "Pause";
highlightNextWord();
}
function stopReader() {
isPlaying = false;
playBtn.textContent = "Play";
clearTimeout(readerInterval);
}
if(pulseLauncher) {
pulseLauncher.addEventListener('click', () => {
if (!verifyIntegrity()) return;
window.speechSynthesis.cancel();
const ttsBar = document.getElementById('tts-control-bar');
if (ttsBar) ttsBar.classList.remove('tts-visible');
pulseControlBar.classList.add('pulse-visible');
toggleWidget();
startReader();
});
}
if(pulseCloseBtn) {
pulseCloseBtn.addEventListener('click', () => {
stopReader();
wordsArray.forEach(el => el.classList.remove('reader-enlarged'));
pulseControlBar.classList.remove('pulse-visible');
});
}
if(playBtn) {
playBtn.addEventListener('click', () => {
if (isPlaying) {
stopReader();
} else {
startReader();
}
});
}
if(speedSlider) {
speedSlider.addEventListener('input', () => {
if (isPlaying) {
clearTimeout(readerInterval);
readerInterval = setTimeout(highlightNextWord, parseInt(speedSlider.value));
}
});
}
window.stopFocusPulse = function() {
stopReader();
wordsArray.forEach(el => el.classList.remove('reader-enlarged'));
pulseControlBar.classList.remove('pulse-visible');
};
});
</script>
Notice:
Comments are moderated and may not appear immediately. Please keep your comments respectful, and relevant to the post. Spam will not be tolerated. My site. My rules.