retroify.js
Version:
A library to apply retro 8-bit styling to web pages
251 lines (229 loc) • 14.1 kB
JavaScript
/**
* retroify.js v0.2.1
* A library to apply retro 8-bit styling to web pages
* https://github.com/rbonestell/retroify.js
*
* @license
* Copyright (c) 2025 Bobby Bonestell
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
(window=>{const features_imageRendering="imageRendering"in(test=document.createElement("div").style)?"imageRendering":"webkitImageRendering"in test?"webkitImageRendering":"mozImageRendering"in test?"mozImageRendering":"msImageRendering"in test?"msImageRendering":"imageRendering";let BLACKLISTED_TAGS=["abbr","acronym","b","bdo","big","br","cite","dfn","em","kbd","label","map","object","output","q","script","small","strong","sub","sup","time","var"];var test=`
/* Basic root element styles */
[data-retroify-root="true"],
[data-retroify="true"] code,
[data-retroify="true"] pre,
[data-retroify="true"] samp {
font-family: 'Press Start 2P', monospace;
font-size: 0.75em;
--retroify-shadow-opacity: 0.3;
}
/* Modified heading sizes */
[data-retroify="true"] h1 {
font-size: 2em;
text-shadow: 2px 2px 0 rgb(var(--retroify-shadow-color, 0 0 0) / var(--retroify-shadow-opacity, 0.3));
}
[data-retroify="true"] h2 {
font-size: 1.75em;
text-shadow: 2px 2px 0 rgb(var(--retroify-shadow-color, 0 0 0) / var(--retroify-shadow-opacity, 0.3));
}
[data-retroify="true"] h3 {
font-size: 1.5em;
text-shadow: 2px 2px 0 rgb(var(--retroify-shadow-color, 0 0 0) / var(--retroify-shadow-opacity, 0.3));
}
[data-retroify="true"] h4 {
font-size: 1.25em;
text-shadow: 1px 1px 0 rgb(var(--retroify-shadow-color, 0 0 0) / var(--retroify-shadow-opacity, 0.3));
}
[data-retroify="true"] h5 {
font-size: 1em;
text-shadow: 1px 1px 0 rgb(var(--retroify-shadow-color, 0 0 0) / var(--retroify-shadow-opacity, 0.3));
}
[data-retroify="true"] h6 {
font-size: 0.8em;
text-shadow: 1px 1px 0 rgb(var(--retroify-shadow-color, 0 0 0) / var(--retroify-shadow-opacity, 0.3));
}
/* Rest of the styles remain the same */
/* Preserve FontAwesome icons */
[data-retroify="true"] i.fa,
[data-retroify="true"] i.fab,
[data-retroify="true"] i.fas {
font-size: 2.08em;
}
/* Image styles */
[data-retroify="true"] img {
box-shadow: 6px 6px 0 rgb(var(--retroify-shadow-color, 0 0 0) / var(--retroify-shadow-opacity, 0.3));
image-rendering: pixelated;
filter: contrast(150%) brightness(110%);
border-radius: 0;
${features_imageRendering}: pixelated;
${features_imageRendering}: -moz-crisp-edges;
${features_imageRendering}: crisp-edges;
}
/* Anchor styles */
[data-retroify="true"] a {
cursor: pointer;
text-shadow: 1px 1px 0 rgb(var(--retroify-shadow-color, 0 0 0) / var(--retroify-shadow-opacity, 0.3));
}
[data-retroify="true"] a:hover {
cursor: pointer;
text-shadow: 2px 2px 0 rgb(var(--retroify-shadow-color, 0 0 0) / var(--retroify-shadow-opacity, 0.3));
}
[data-retroify="true"] a:active {
top: 5px;
left: 5px;
cursor: pointer;
text-shadow: none;
}
/* Button styles */
[data-retroify="true"] button,
[data-retroify="true"] input[type="button"],
[data-retroify="true"] input[type="submit"],
[data-retroify="true"] .btn {
position: relative;
top: 0;
left: 0;
margin-right: 3px;
margin-bottom: 3px;
transform: scale(1);
transition: all 0.1s ease;
border-radius: 0 !important;
padding: 12px;
border-width: 2px;
border-style: solid;
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.3);
font-size: 0.8em;
}
/* Hover states for buttons */
[data-retroify="true"] button:hover,
[data-retroify="true"] input[type="button"]:hover,
[data-retroify="true"] input[type="submit"]:hover,
[data-retroify="true"] .btn:hover {
top: -2px;
left: -2px;
cursor: pointer;
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.3);
border-radius: 0 !important;
}
/* Active states for buttons */
[data-retroify="true"] button:active,
[data-retroify="true"] input[type="button"]:active,
[data-retroify="true"] input[type="submit"]:active,
[data-retroify="true"] .btn:active {
top: 4px !important;
left: 4px !important;
box-shadow: 0px 0px 0 !important;
border-radius: 0 !important;
}
[data-retroify="true"] .input-group-text,
[data-retroify="true"] input:not([type="button"]):not([type="submit"]),
[data-retroify="true"] textarea {
border: 2px solid currentColor !important;
border-radius: 0 !important;
box-shadow: 4px 4px 0 rgb(var(--retroify-shadow-color, 0 0 0) / var(--retroify-shadow-opacity, 0.3));
padding: 6px !important;
font-size: 0.9em;
}
[data-retroify="true"] select {
border: 2px solid currentColor !important;
border-radius: 0 !important;
box-shadow: 4px 4px 0 rgb(var(--retroify-shadow-color, 0 0 0) / var(--retroify-shadow-opacity, 0.3));
padding: 6px !important;
font-size: 0.9em;
}
[data-retroify="true"] .card,
[data-retroify="true"] .alert,
[data-retroify="true"] .modal-content {
border-radius: 0 !important;
box-shadow: 4px 4px 0 rgb(var(--retroify-shadow-color, 0 0 0) / var(--retroify-shadow-opacity, 0.3)) !important;
border: 2px solid currentColor !important;
}
[data-retroify="true"] hr {
border: none;
border-bottom: 4px dashed currentColor;
margin: 2em 0;
}
[data-retroify="true"] table {
border-collapse: separate;
border-spacing: 0;
border: 2px solid currentColor;
box-shadow: 4px 4px 0 rgb(var(--retroify-shadow-color, 0 0 0) / var(--retroify-shadow-opacity, 0.3));
}
[data-retroify="true"] th {
border: 1px solid currentColor;
padding: 8px;
text-shadow: 1px 1px 0 rgb(var(--retroify-shadow-color, 0 0 0) / var(--retroify-shadow-opacity, 0.3));
}
[data-retroify="true"] td {
border: 1px solid currentColor;
padding: 8px;
}
.retroify-scanlines::before {
content: "";
position: fixed;
top: -50px;
left: 0;
width: 100%;
height: calc(100% + 50px);
background: repeating-linear-gradient(
0deg,
rgb(var(--retroify-shadow-color, 0 0 0) / 0.03),
rgb(var(--retroify-shadow-color, 0 0 0) / 0.03) 2px,
transparent 3px,
transparent 5px
);
pointer-events: none;
z-index: 9999;
animation: retroify-scanline-scroll 8s linear infinite;
}
retroify-scanline-scroll {
0% { transform: translateY(0); }
100% { transform: translateY(50px); }
}
retroify-float {
0% { transform: translateY(0); }
50% { transform: translateY(-4px); }
100% { transform: translateY(0); }
}
[data-retroify="true"] .floating {
animation: retroify-float 2s ease-in-out infinite;
}
[data-retroify="true"] .blink {
animation: retroify-blink 1s step-end infinite;
}
retroify-blink {
50% { opacity: 0; }
}
/* Reduced Motion */
(prefers-reduced-motion: reduce) {
[data-retroify="true"] .floating {
animation: none !important;
}
[data-retroify="true"] button,
[data-retroify="true"] .btn {
transition: none !important;
}
}
/* Print styles */
print {
.retroify-scanlines::before {
display: none !important;
}
}
`,styleSheet=(document.querySelector("style[data-retroify]")||((styleSheet=document.createElement("style")).setAttribute("data-retroify",""),styleSheet.textContent=test,document.head.appendChild(styleSheet)),document.querySelector('link[href*="Press+Start+2P"]')||((test=document.createElement("link")).href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap",test.rel="stylesheet",document.head.appendChild(test)),{version:"0.2.1",isSupported:function(){return void 0!==window&&"undefined"!=typeof document&&"querySelector"in document&&"classList"in document.documentElement},_init:function(){return!!this.isSupported()||(console.warn("Retroify: Browser environment not supported"),!1)},apply:function(target){if(!this._init())return this;let applyToTargetElement=element=>{element._originalStyle||(element._originalStyle=element.style),element.setAttribute("data-retroify","true"),"button"===element.tagName.toLowerCase()&&element.setAttribute("aria-pressed","false");var child,selector=`*:not(${BLACKLISTED_TAGS.join(", ")})`;for(child of element.querySelectorAll(selector))child.setAttribute("data-retroify","true"),"button"===child.tagName.toLowerCase()&&child.setAttribute("aria-pressed","false")};try{let rootElement;if(target?(rootElement="string"==typeof target?document.querySelector(target):target)==document.body&&this.toggleScanlines(!0):(rootElement=document.body,this.toggleScanlines(!0)),!rootElement)throw new Error("Target element not found");if(!(rootElement instanceof HTMLElement))throw new Error("Invalid target type. Expected HTMLElement or string selector");rootElement.setAttribute("data-retroify-root","true"),applyToTargetElement(rootElement);var observer=new MutationObserver(mutations=>{for(var mutation of mutations)if("childList"===mutation.type)for(var node of mutation.addedNodes)node.nodeType===Node.ELEMENT_NODE&&node.parentElement&&"true"===node.parentElement.getAttribute("data-retroify")&&applyToTargetElement(node)});return observer.observe(rootElement,{childList:!0,subtree:!0}),rootElement._retroifyObserver=observer,window.dispatchEvent(new CustomEvent("retroify:applied",{detail:{target:rootElement}})),this}catch(e){return console.error("Retroify:",e.message),this}},remove:function(target){if(!this._init())return this;try{var rootElement=target?"string"==typeof target?document.querySelector(target):target:document.body;if(rootElement)return rootElement._retroifyObserver&&(rootElement._retroifyObserver.disconnect(),delete rootElement._retroifyObserver),rootElement._originalStyle&&(Object.assign(rootElement.style,rootElement._originalStyle),delete rootElement._originalStyle),rootElement.removeAttribute("data-retroify"),rootElement.removeAttribute("data-retroify-root"),rootElement.querySelectorAll("[data-retroify]").forEach(element=>{element.removeAttribute("data-retroify"),element._originalStyle&&(Object.assign(element.style,element._originalStyle),delete element._originalStyle)}),window.dispatchEvent(new CustomEvent("retroify:removed",{detail:{target:rootElement}})),this;throw new Error("Target element not found")}catch(e){return console.error("Retroify:",e.message),this}},toggle:function(){return document.body.hasAttribute("data-retroify")?(this.toggleScanlines(!1),this.remove(document.body)):(this.toggleScanlines(!0),this.apply(document.body)),this},toggleScanlines:function(enable){return"boolean"==typeof enable?enable?document.body.classList.add("retroify-scanlines"):document.body.classList.remove("retroify-scanlines"):document.body.classList.toggle("retroify-scanlines"),this},destroy:function(){this.remove(),this.toggleScanlines(!1);var style=document.querySelector("style[data-retroify]");return style&&style.remove(),this},_validateSelector:function(selector){try{return document.querySelector(selector),!0}catch(e){return console.warn("Retroify: Invalid selector syntax"),!1}}});document.addEventListener("error",function(e){"LINK"===e.target.tagName&&e.target.href.includes("Press+Start+2P")&&console.warn("Retroify: Failed to load Press Start 2P font. Falling back to system fonts.")},!0),window.addEventListener("unload",()=>{window.retroify&&window.retroify.destroy()}),void 0===window.retroify&&Object.defineProperty(window,"retroify",{value:Object.freeze(styleSheet),writable:!1,configurable:!1})})("undefined"!=typeof window?window:this);