- Add comprehensive JAV studios quick reference guide - Update documentation index with JAV reference - Add logo animation components and test files - Update CSS styling for cards, buttons, forms, and theme - Add utility scripts for configuration and import workflows - Update templates and UI components 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
311 lines
11 KiB
HTML
311 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Logo Animation Test</title>
|
|
<style>
|
|
body { background: #1a1a1a; color: white; padding: 2rem; font-family: Arial, sans-serif; }
|
|
.logo { margin: 2rem 0; width: 180px; height: 110px; }
|
|
.logo svg { width: 100%; height: 100%; display: block; }
|
|
.goondex-logo-animated .breast-left,
|
|
.goondex-logo-animated .breast-right {
|
|
animation: breastBounce 1.6s ease-in-out infinite;
|
|
transform-origin: center center;
|
|
}
|
|
.goondex-logo-animated .breast-right { animation-delay: 0.08s; }
|
|
.goondex-logo-animated .nipple-left,
|
|
.goondex-logo-animated .nipple-right {
|
|
animation: nippleBob 1.6s ease-in-out infinite;
|
|
transform-origin: center center;
|
|
}
|
|
.goondex-logo-animated .nipple-right { animation-delay: 0.12s; }
|
|
|
|
@keyframes breastBounce {
|
|
0% { transform: translateY(0) scale(1); }
|
|
12% { transform: translateY(-4px) scaleX(1.01) scaleY(0.985); }
|
|
28% { transform: translateY(8px) scaleX(0.99) scaleY(1.03); }
|
|
44% { transform: translateY(-3px) scaleX(1.012) scaleY(0.988); }
|
|
60% { transform: translateY(4px) scaleX(0.995) scaleY(1.015); }
|
|
100% { transform: translateY(0) scale(1); }
|
|
}
|
|
|
|
@keyframes nippleBob {
|
|
0%, 100% { transform: translate(0, 0); }
|
|
18% { transform: translate(0px, -5px) scale(1.03); }
|
|
35% { transform: translate(0px, 6px) scale(0.98); }
|
|
55% { transform: translate(0px, -3px) scale(1.02); }
|
|
75% { transform: translate(0px, 2px); }
|
|
}
|
|
|
|
button { background: #ff5fa2; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; margin-right: 1rem; cursor: pointer; }
|
|
.global-loader {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.55);
|
|
backdrop-filter: blur(2px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 2000;
|
|
}
|
|
.global-loader .loader-content {
|
|
background: #2a2a2a;
|
|
padding: 1.5rem 2rem;
|
|
border-radius: 12px;
|
|
border: 1px solid #444;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
color: white;
|
|
min-width: 280px;
|
|
justify-content: center;
|
|
}
|
|
.global-loader .logo svg {
|
|
width: 90px;
|
|
height: 55px;
|
|
filter: drop-shadow(0 2px 8px rgba(255, 95, 162, 0.3));
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Goondex Logo Animation Test</h1>
|
|
|
|
<div style="margin: 2rem 0;">
|
|
<h2>Static Logo:</h2>
|
|
<div id="static-logo" class="logo"></div>
|
|
</div>
|
|
|
|
<div style="margin: 2rem 0;">
|
|
<h2>Animated Logo:</h2>
|
|
<div id="animated-logo" class="logo"></div>
|
|
</div>
|
|
|
|
<div style="margin: 2rem 0;">
|
|
<button onclick="startAnimation()">Start Animation</button>
|
|
<button onclick="stopAnimation()">Stop Animation</button>
|
|
</div>
|
|
|
|
<div style="margin: 2rem 0;">
|
|
<button onclick="testLoader()">Test Loader (3 seconds)</button>
|
|
</div>
|
|
|
|
<div id="global-loader" class="global-loader" style="display:none;">
|
|
<div class="loader-content">
|
|
<div id="loader-logo" class="logo"></div>
|
|
<div>Working...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
class LogoAnimator {
|
|
constructor() {
|
|
this.isAnimating = false;
|
|
this.logoElement = null;
|
|
}
|
|
|
|
init(svgElement) {
|
|
this.logoElement = svgElement;
|
|
this.identifyParts();
|
|
}
|
|
|
|
identifyParts() {
|
|
if (!this.logoElement) return;
|
|
const nipples = [];
|
|
const breasts = [];
|
|
|
|
// Prefer elements with ids/classes if present
|
|
const breastCandidates = [
|
|
this.logoElement.querySelector('#breast-left'),
|
|
this.logoElement.querySelector('#breast-right')
|
|
].filter(Boolean);
|
|
const nippleCandidates = [
|
|
this.logoElement.querySelector('#nipple-left'),
|
|
this.logoElement.querySelector('#nipple-right')
|
|
].filter(Boolean);
|
|
|
|
breasts.push(...breastCandidates);
|
|
nipples.push(...nippleCandidates);
|
|
|
|
// Fallback nipples: first two circles/ellipses
|
|
if (nipples.length < 2) {
|
|
const circ = Array.from(this.logoElement.querySelectorAll('circle, ellipse'));
|
|
while (nipples.length < 2 && circ.length) {
|
|
nipples.push(circ.shift());
|
|
}
|
|
}
|
|
|
|
// Fallback breasts: first two paths/shapes
|
|
if (breasts.length < 2) {
|
|
const shapes = Array.from(this.logoElement.querySelectorAll('path, polygon, rect'));
|
|
while (breasts.length < 2 && shapes.length) {
|
|
breasts.push(shapes.shift());
|
|
}
|
|
}
|
|
|
|
// Ultimate fallback: animate whole svg as a single breast pair
|
|
if (breasts.length === 0) breasts.push(this.logoElement);
|
|
if (breasts.length === 1) breasts.push(this.logoElement);
|
|
|
|
if (breasts[0]) breasts[0].classList.add('breast-left');
|
|
if (breasts[1]) breasts[1].classList.add('breast-right');
|
|
|
|
if (nipples.length === 0) {
|
|
// If no explicit nipples, piggyback on breasts so some motion happens
|
|
nipples.push(breasts[0], breasts[1]);
|
|
}
|
|
nipples.slice(0, 2).forEach((el, idx) => {
|
|
if (el) el.classList.add(idx === 0 ? 'nipple-left' : 'nipple-right');
|
|
});
|
|
}
|
|
|
|
startBounce() {
|
|
if (!this.logoElement || this.isAnimating) return;
|
|
|
|
this.logoElement.classList.add('goondex-logo-animated');
|
|
this.isAnimating = true;
|
|
}
|
|
|
|
stopBounce() {
|
|
if (!this.logoElement) return;
|
|
|
|
this.logoElement.classList.remove('goondex-logo-animated');
|
|
this.isAnimating = false;
|
|
}
|
|
}
|
|
|
|
// Inline-load the SVG so we can animate internals
|
|
// INLINE ANIMATOR (self-contained for this test page)
|
|
class LogoAnimator {
|
|
constructor() {
|
|
this.isAnimating = false;
|
|
this.logoElement = null;
|
|
}
|
|
|
|
init(svgElement) {
|
|
this.logoElement = svgElement;
|
|
this.identifyParts();
|
|
}
|
|
|
|
identifyParts() {
|
|
if (!this.logoElement) return;
|
|
const nipples = [];
|
|
const breasts = [];
|
|
|
|
const breastCandidates = [
|
|
this.logoElement.querySelector('#breast-left'),
|
|
this.logoElement.querySelector('#breast-right')
|
|
].filter(Boolean);
|
|
const nippleCandidates = [
|
|
this.logoElement.querySelector('#nipple-left'),
|
|
this.logoElement.querySelector('#nipple-right')
|
|
].filter(Boolean);
|
|
|
|
breasts.push(...breastCandidates);
|
|
nipples.push(...nippleCandidates);
|
|
|
|
if (nipples.length < 2) {
|
|
const circ = Array.from(this.logoElement.querySelectorAll('circle, ellipse'));
|
|
while (nipples.length < 2 && circ.length) nipples.push(circ.shift());
|
|
}
|
|
if (breasts.length < 2) {
|
|
const shapes = Array.from(this.logoElement.querySelectorAll('path, polygon, rect'));
|
|
while (breasts.length < 2 && shapes.length) breasts.push(shapes.shift());
|
|
}
|
|
if (breasts.length === 0) breasts.push(this.logoElement);
|
|
if (breasts.length === 1) breasts.push(this.logoElement);
|
|
|
|
if (breasts[0]) breasts[0].classList.add('breast-left');
|
|
if (breasts[1]) breasts[1].classList.add('breast-right');
|
|
|
|
if (nipples.length === 0) nipples.push(breasts[0], breasts[1]);
|
|
nipples.slice(0, 2).forEach((el, idx) => el && el.classList.add(idx === 0 ? 'nipple-left' : 'nipple-right'));
|
|
}
|
|
|
|
startBounce() {
|
|
if (!this.logoElement || this.isAnimating) return;
|
|
this.logoElement.classList.add('goondex-logo-animated');
|
|
this.isAnimating = true;
|
|
}
|
|
|
|
stopBounce() {
|
|
if (!this.logoElement) return;
|
|
this.logoElement.classList.remove('goondex-logo-animated');
|
|
this.isAnimating = false;
|
|
}
|
|
}
|
|
|
|
async function loadSVG(urls, targetId) {
|
|
const target = document.getElementById(targetId);
|
|
if (!target) return null;
|
|
for (const url of urls) {
|
|
try {
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error('fetch failed');
|
|
const svgText = await res.text();
|
|
target.innerHTML = svgText;
|
|
const svg = target.querySelector('svg');
|
|
return svg;
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
}
|
|
// Fallback to img if all fetches fail (no animation possible)
|
|
target.innerHTML = `<img src=\"${urls[0]}\" alt=\"Goondex Logo\" width=\"100%\" height=\"100%\">`;
|
|
return null;
|
|
}
|
|
|
|
const logoURLs = [
|
|
"/static/img/logo/GOONDEX_Titty.svg",
|
|
"static/img/logo/GOONDEX_Titty.svg",
|
|
"./static/img/logo/GOONDEX_Titty.svg"
|
|
];
|
|
|
|
let animator = null;
|
|
let loaderAnimator = null;
|
|
|
|
async function initLogos() {
|
|
const staticSvg = await loadSVG(logoURLs, 'static-logo');
|
|
const animatedSvg = await loadSVG(logoURLs, 'animated-logo');
|
|
const loaderSvg = await loadSVG(logoURLs, 'loader-logo');
|
|
|
|
if (animatedSvg) {
|
|
animator = new LogoAnimator();
|
|
animator.init(animatedSvg);
|
|
animator.startBounce();
|
|
}
|
|
if (loaderSvg) {
|
|
loaderAnimator = new LogoAnimator();
|
|
loaderAnimator.init(loaderSvg);
|
|
}
|
|
}
|
|
|
|
function startAnimation() {
|
|
if (animator) animator.startBounce();
|
|
}
|
|
|
|
function stopAnimation() {
|
|
if (animator) animator.stopBounce();
|
|
}
|
|
|
|
function testLoader() {
|
|
const loader = document.getElementById('global-loader');
|
|
loader.style.display = 'flex';
|
|
|
|
if (loaderAnimator) {
|
|
loaderAnimator.startBounce();
|
|
}
|
|
|
|
setTimeout(() => {
|
|
loader.style.display = 'none';
|
|
if (loaderAnimator) {
|
|
loaderAnimator.stopBounce();
|
|
}
|
|
}, 3000);
|
|
}
|
|
|
|
initLogos();
|
|
</script>
|
|
</body>
|
|
</html>
|