Content Creator · Bot Maker

dark romance
& worlds built
from nothing

Brazilian creator behind original bots and anime character worlds on JanitorAI. Dark romance, enemies-to-lovers, and stories that pull you under.

madokka avatar
madokka
@ogum · janitorai

Creating immersive male bots for a female audience. Original series, anime fandom — all wrapped in dark aesthetic and obsessive lore.

Dark RomanceEnemies to LoversBot MakerAnime
(function(){ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; document.body.dataset.theme = localStorage.getItem('theme') || (prefersDark ? 'dark' : 'light'); window.addEventListener('madokkaThemeChange', e => { if (e.detail?.theme) document.body.dataset.theme = e.detail.theme; }); const cv=document.getElementById('homeHeroWater'); if(cv){const cx=cv.getContext('2d');let t=0;const rs=()=>{cv.width=window.innerWidth;cv.height=window.innerHeight;};rs();window.addEventListener('resize',rs);(function draw(){cx.clearRect(0,0,cv.width,cv.height);const col=document.body.dataset.theme!=='light'?'200,133,74':'122,74,32';for(let i=0;i<8;i++){cx.beginPath();const a=24+i*9,f=.005+i*.001,s=.007+i*.003,y=(cv.height/8)*i;cx.moveTo(0,y);for(let x=0;x<=cv.width;x+=4)cx.lineTo(x,y+Math.sin(x*f+t*s+i)*a+Math.cos(x*f*.5+t*s*.65)*(a*.36));cx.strokeStyle=`rgba(${col},${.035+i*.006})`;cx.lineWidth=1.5;cx.stroke();}t++;requestAnimationFrame(draw);})();} document.getElementById('homeHeroPage')?.classList.add('visible'); setTimeout(()=>{document.getElementById('homeHeroText')?.classList.add('in');document.getElementById('homeHeroVisual')?.classList.add('in');},150); })();
Highlights

What's happening
in my world

(function(){ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; document.body.dataset.theme = localStorage.getItem('theme') || (prefersDark ? 'dark' : 'light'); window.addEventListener('madokkaThemeChange', e => { if (e.detail?.theme) document.body.dataset.theme = e.detail.theme; }); const cv=document.getElementById('homeHighlightsWater'); if(cv){const cx=cv.getContext('2d');let t=0;const rs=()=>{cv.width=window.innerWidth;cv.height=window.innerHeight;};rs();window.addEventListener('resize',rs);(function draw(){cx.clearRect(0,0,cv.width,cv.height);const col=document.body.dataset.theme!=='light'?'200,133,74':'122,74,32';for(let i=0;i<6;i++){cx.beginPath();const a=24+i*9,f=.005+i*.001,s=.007+i*.003,y=(cv.height/6)*i;cx.moveTo(0,y);for(let x=0;x<=cv.width;x+=4)cx.lineTo(x,y+Math.sin(x*f+t*s+i)*a+Math.cos(x*f*.5+t*s*.65)*(a*.36));cx.strokeStyle=`rgba(${col},${.035+i*.006})`;cx.lineWidth=1.5;cx.stroke();}t++;requestAnimationFrame(draw);})();} document.getElementById('homeHighlightsPage')?.classList.add('visible'); setTimeout(()=>{document.getElementById('homeHighlightsEyebrow')?.classList.add('in');document.getElementById('homeHighlightsTitle')?.classList.add('in');document.getElementById('homeHighlightsCarousel')?.classList.add('in');document.getElementById('homeHighlightsControls')?.classList.add('in');},100); const track=document.getElementById('homeHighlightsTrack'),dotsEl=document.getElementById('homeHighlightsDots'),prevBtn=document.getElementById('homeHighlightsPrev'),nextBtn=document.getElementById('homeHighlightsNext'); if(track&&dotsEl){const cards=Array.from(track.children);const total=cards.length;let cur=0;if(total){for(let i=0;igo(i));dotsEl.appendChild(d);}function go(idx){cur=(idx+total)%total;const w=cards[0].offsetWidth+20;track.style.transform=`translateX(-${cur*w}px)`;dotsEl.querySelectorAll('.c-dot').forEach((d,i)=>d.classList.toggle('active',i===cur));}prevBtn?.addEventListener('click',()=>go(cur-1));nextBtn?.addEventListener('click',()=>go(cur+1));setInterval(()=>go(cur+1),5200);let sx=0;track.addEventListener('touchstart',e=>sx=e.touches[0].clientX,{passive:true});track.addEventListener('touchend',e=>{const d=sx-e.changedTouches[0].clientX;if(Math.abs(d)>50)go(d>0?cur+1:cur-1);});}} })(); (function () { function initHomeHighlightsCarousel() { const track = document.getElementById('homeHighlightsTrack'); const dotsEl = document.getElementById('homeHighlightsDots'); const prevBtn = document.getElementById('homeHighlightsPrev'); const nextBtn = document.getElementById('homeHighlightsNext'); if (!track || !dotsEl || !prevBtn || !nextBtn) return; const cards = Array.from(track.children); const total = cards.length; let current = 0; if (!total) return; dotsEl.innerHTML = ''; cards.forEach((_, i) => { const dot = document.createElement('button'); dot.type = 'button'; dot.className = 'c-dot' + (i === 0 ? ' active' : ''); dot.setAttribute('aria-label', 'Go to slide ' + (i + 1)); dot.addEventListener('click', function () { goToSlide(i); }); dotsEl.appendChild(dot); }); function getStep() { const card = cards[0]; if (!card) return 0; const style = window.getComputedStyle(track); const gap = parseFloat(style.columnGap || style.gap || 20) || 20; return card.getBoundingClientRect().width + gap; } function goToSlide(index) { current = (index + total) % total; const maxVisibleOffset = Math.max(0, track.scrollWidth - track.parentElement.clientWidth); const targetOffset = Math.min(current * getStep(), maxVisibleOffset); track.style.transform = 'translateX(-' + targetOffset + 'px)'; dotsEl.querySelectorAll('.c-dot').forEach((dot, i) => { dot.classList.toggle('active', i === current); }); } prevBtn.type = 'button'; nextBtn.type = 'button'; prevBtn.addEventListener('click', function () { goToSlide(current - 1); }); nextBtn.addEventListener('click', function () { goToSlide(current + 1); }); let startX = 0; track.addEventListener('touchstart', function (e) { startX = e.touches[0].clientX; }, { passive: true }); track.addEventListener('touchend', function (e) { const diff = startX - e.changedTouches[0].clientX; if (Math.abs(diff) > 50) { goToSlide(diff > 0 ? current + 1 : current - 1); } }); window.addEventListener('resize', function () { goToSlide(current); }); goToSlide(0); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initHomeHighlightsCarousel); } else { setTimeout(initHomeHighlightsCarousel, 100); } })();

Support the work

Every bot, every series, every late-night writing session — if my work has meant something to you, you can send a little love directly. Donations are in USD via PayPal.

$
Send via PayPal

Donations go directly to [email protected]

(function(){ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; document.body.dataset.theme = localStorage.getItem('theme') || (prefersDark ? 'dark' : 'light'); window.addEventListener('madokkaThemeChange', e => { if (e.detail?.theme) document.body.dataset.theme = e.detail.theme; }); const cv=document.getElementById('homeSupportWater'); if(cv){const cx=cv.getContext('2d');let t=0;const rs=()=>{cv.width=window.innerWidth;cv.height=window.innerHeight;};rs();window.addEventListener('resize',rs);(function draw(){cx.clearRect(0,0,cv.width,cv.height);const col=document.body.dataset.theme!=='light'?'200,133,74':'122,74,32';for(let i=0;i<6;i++){cx.beginPath();const a=24+i*9,f=.005+i*.001,s=.007+i*.003,y=(cv.height/6)*i;cx.moveTo(0,y);for(let x=0;x<=cv.width;x+=4)cx.lineTo(x,y+Math.sin(x*f+t*s+i)*a+Math.cos(x*f*.5+t*s*.65)*(a*.36));cx.strokeStyle=`rgba(${col},${.035+i*.006})`;cx.lineWidth=1.5;cx.stroke();}t++;requestAnimationFrame(draw);})();} document.getElementById('homeSupportPage')?.classList.add('visible'); const obs=new IntersectionObserver(entries=>{entries.forEach(e=>{if(e.isIntersecting){e.target.classList.add('in');obs.unobserve(e.target);}});},{threshold:.08}); document.querySelectorAll('#homeSupportPage .rv').forEach(el=>obs.observe(el)); const chips=document.querySelectorAll('#homeSupportPage .chip'),amtInput=document.getElementById('homeSupportAmount'),ppLink=document.getElementById('homeSupportPaypalLink'); function updateLink(v){if(!ppLink)return;const n=parseFloat(v)||10;ppLink.href=`https://www.paypal.com/paypalme/ismadokka/${n}`;} chips.forEach(c=>c.addEventListener('click',()=>{chips.forEach(x=>x.classList.remove('sel'));c.classList.add('sel');if(amtInput)amtInput.value=c.dataset.v;updateLink(c.dataset.v);})); amtInput?.addEventListener('input',()=>{chips.forEach(x=>x.classList.remove('sel'));updateLink(amtInput.value);});updateLink(10); })(); (function () { const obs = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); obs.unobserve(e.target); } }); }, { threshold: .08 }); document.querySelectorAll('#homeSupportPage .rv').forEach(el => obs.observe(el)); const chips = document.querySelectorAll('#homeSupportPage .chip'); const amtInput = document.getElementById('homeSupportAmount'); const ppLink = document.getElementById('homeSupportPaypalLink'); function updateLink(v) { if (!ppLink) return; const n = parseFloat(v) || 10; ppLink.href = `https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=ismadokka%40gmail.com&item_name=Support%20Madokka¤cy_code=USD&amount=${n}`; } chips.forEach(c => { c.addEventListener('click', () => { chips.forEach(x => x.classList.remove('sel')); c.classList.add('sel'); if (amtInput) amtInput.value = c.dataset.v; updateLink(c.dataset.v); }); }); amtInput?.addEventListener('input', () => { chips.forEach(x => x.classList.remove('sel')); updateLink(amtInput.value); }); updateLink(10); })();
Madokka
The person behind the bots

madokka
vt.

@ogum · creator · writer

Brazilian content creator with a taste for the dark and complicated. I build male bots for a female audience on JanitorAI — original characters, anime figures, and men who will ruin your day in the best possible way.

When I'm not writing bot personas, I'm watching anime, consuming dark romance at an alarming rate, or finding new ways to make the internet feel like a cozy, dimly lit café at midnight.

5Original Series
18+Bots Created
2Accounts
Dark RomanceBot MakerAnimeBrazilJanitorAI
(function(){ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; document.body.dataset.theme = localStorage.getItem('theme') || (prefersDark ? 'dark' : 'light'); window.addEventListener('madokkaThemeChange', e => { if (e.detail?.theme) document.body.dataset.theme = e.detail.theme; }); const cv=document.getElementById('aboutBioWater'); if(cv){const cx=cv.getContext('2d');let t=0;const rs=()=>{cv.width=window.innerWidth;cv.height=window.innerHeight;};rs();window.addEventListener('resize',rs);(function draw(){cx.clearRect(0,0,cv.width,cv.height);const col=document.body.dataset.theme!=='light'?'200,133,74':'122,74,32';for(let i=0;i<7;i++){cx.beginPath();const a=24+i*9,f=.005+i*.001,s=.007+i*.003,y=(cv.height/7)*i;cx.moveTo(0,y);for(let x=0;x<=cv.width;x+=4)cx.lineTo(x,y+Math.sin(x*f+t*s+i)*a+Math.cos(x*f*.5+t*s*.65)*(a*.36));cx.strokeStyle=`rgba(${col},${.035+i*.006})`;cx.lineWidth=1.5;cx.stroke();}t++;requestAnimationFrame(draw);})();} document.getElementById('aboutBioPage')?.classList.add('visible');setTimeout(()=>{document.getElementById('aboutBioImageWrap')?.classList.add('in');document.getElementById('aboutBioText')?.classList.add('in');},120); })();
Visual World

gallery & archives

🗂Bots & Characters
Original character concepts, reference images, and visual moodboards for each series.
🌙Aesthetic & Mood
Curated moodboards that inspire the world I create — dark, warm, and a little obsessive.
📖Anime Inspo
Screencaps, fanart references, and character studies from the media that lives in my head 24/7.
(function(){ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; document.body.dataset.theme = localStorage.getItem('theme') || (prefersDark ? 'dark' : 'light'); window.addEventListener('madokkaThemeChange', e => { if (e.detail?.theme) document.body.dataset.theme = e.detail.theme; }); const cv=document.getElementById('aboutGalleryWater'); if(cv){const cx=cv.getContext('2d');let t=0;const rs=()=>{cv.width=window.innerWidth;cv.height=window.innerHeight;};rs();window.addEventListener('resize',rs);(function draw(){cx.clearRect(0,0,cv.width,cv.height);const col=document.body.dataset.theme!=='light'?'200,133,74':'122,74,32';for(let i=0;i<6;i++){cx.beginPath();const a=24+i*9,f=.005+i*.001,s=.007+i*.003,y=(cv.height/6)*i;cx.moveTo(0,y);for(let x=0;x<=cv.width;x+=4)cx.lineTo(x,y+Math.sin(x*f+t*s+i)*a+Math.cos(x*f*.5+t*s*.65)*(a*.36));cx.strokeStyle=`rgba(${col},${.035+i*.006})`;cx.lineWidth=1.5;cx.stroke();}t++;requestAnimationFrame(draw);})();} document.getElementById('aboutGalleryPage')?.classList.add('visible'); const obs=new IntersectionObserver(entries=>{entries.forEach(e=>{if(e.isIntersecting){e.target.classList.add('in');obs.unobserve(e.target);}});},{threshold:.08});document.querySelectorAll('#aboutGalleryPage .rv').forEach(el=>obs.observe(el)); document.querySelectorAll('#aboutGalleryPage .thumb').forEach(th=>{th.addEventListener('click',()=>{const img=document.getElementById('aboutGalleryLightboxImg'),cap=document.getElementById('aboutGalleryLightboxCaption'),lb=document.getElementById('aboutGalleryLightbox');if(img)img.src=th.dataset.src;if(cap)cap.textContent=th.dataset.cap||'';lb?.classList.add('open');});}); window.aboutGalleryCloseLightbox=function(){document.getElementById('aboutGalleryLightbox')?.classList.remove('open');}; const lb=document.getElementById('aboutGalleryLightbox');lb?.addEventListener('click',e=>{if(e.target===e.currentTarget)window.aboutGalleryCloseLightbox();});document.addEventListener('keydown',e=>{if(e.key==='Escape')window.aboutGalleryCloseLightbox();}); window.aboutGalleryToggleFolder=function(head){const body=head.nextElementSibling,chev=head.querySelector('.f-chev'),open=body.classList.contains('open');body.classList.toggle('open',!open);if(chev)chev.style.transform=open?'':'rotate(180deg)';}; })();
What I'm Listening To

current playlist

0:00
(function(){ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; document.body.dataset.theme = localStorage.getItem('theme') || (prefersDark ? 'dark' : 'light'); window.addEventListener('madokkaThemeChange', e => { if (e.detail?.theme) document.body.dataset.theme = e.detail.theme; }); const cv=document.getElementById('aboutPlaylistWater'); if(cv){const cx=cv.getContext('2d');let t=0;const rs=()=>{cv.width=window.innerWidth;cv.height=window.innerHeight;};rs();window.addEventListener('resize',rs);(function draw(){cx.clearRect(0,0,cv.width,cv.height);const col=document.body.dataset.theme!=='light'?'200,133,74':'122,74,32';for(let i=0;i<6;i++){cx.beginPath();const a=24+i*9,f=.005+i*.001,s=.007+i*.003,y=(cv.height/6)*i;cx.moveTo(0,y);for(let x=0;x<=cv.width;x+=4)cx.lineTo(x,y+Math.sin(x*f+t*s+i)*a+Math.cos(x*f*.5+t*s*.65)*(a*.36));cx.strokeStyle=`rgba(${col},${.035+i*.006})`;cx.lineWidth=1.5;cx.stroke();}t++;requestAnimationFrame(draw);})();} document.getElementById('aboutPlaylistPage')?.classList.add('visible'); const obs=new IntersectionObserver(entries=>{entries.forEach(e=>{if(e.isIntersecting){e.target.classList.add('in');obs.unobserve(e.target);}});},{threshold:.08});document.querySelectorAll('#aboutPlaylistPage .rv').forEach(el=>obs.observe(el)); const tracks=[ {title:'Papoulas',artist:'Yago Oproprio, Patricio Sid',dur:'3:59',total:239,cover:'https://file.garden/aeWGq38drAn4J58M/spotify/artworks-gR5Za2zIzbeS-0-t1080x1080.webp'}, {title:'Fantasma',artist:'Luan Santana, Marília Mendonça',dur:'2:58',total:178,cover:'https://file.garden/aeWGq38drAn4J58M/spotify/1900x1900-000000-80-0-0.jpg'}, {title:'Need Me',artist:'Rihanna',dur:'4:21',total:261,cover:'https://file.garden/aeWGq38drAn4J58M/spotify/rihanna-anti-album-cover-2-820x820.webp'}, {title:'Falando com as Favelas',artist:'Memphis, Hariel',dur:'3:31',total:211,cover:'https://file.garden/aeWGq38drAn4J58M/spotify/ab67616d0000b2731dc760e18312cef8deacfcd1.jpg'}, {title:'Cobras Fumantes',artist:'Sabaton',dur:'4:00',total:240,cover:'https://file.garden/aeWGq38drAn4J58M/spotify/smoke-snake.jpg'}, ]; let cur=0,playing=false,prog=0,pInt=null;function el(id){return document.getElementById(id);} function updateUI(){ if(!tracks.length)return; const tr=tracks[cur], p=tracks[(cur-1+tracks.length)%tracks.length], n=tracks[(cur+1)%tracks.length]; if(el('aboutPlaylistAlbumPrev'))el('aboutPlaylistAlbumPrev').src=p.cover; if(el('aboutPlaylistAlbumActive'))el('aboutPlaylistAlbumActive').src=tr.cover; if(el('aboutPlaylistAlbumNext'))el('aboutPlaylistAlbumNext').src=n.cover; if(el('aboutPlaylistNowTitle'))el('aboutPlaylistNowTitle').textContent=tr.title; if(el('aboutPlaylistNowArtist'))el('aboutPlaylistNowArtist').textContent=tr.artist; if(el('aboutPlaylistTimeTotal'))el('aboutPlaylistTimeTotal').textContent=tr.dur; document.querySelectorAll('#aboutPlaylistTrackList .spotify-embed').forEach((frame,i)=>{ frame.classList.toggle('active',i===cur); }); prog=0; if(el('aboutPlaylistProgressFill'))el('aboutPlaylistProgressFill').style.width='0%'; if(el('aboutPlaylistTimeNow'))el('aboutPlaylistTimeNow').textContent='0:00'; } window.aboutPlaylistChangeTo=function(i){cur=i;updateUI();};window.aboutPlaylistNext=function(){window.aboutPlaylistChangeTo((cur+1)%tracks.length);};window.aboutPlaylistPrev=function(){window.aboutPlaylistChangeTo((cur-1+tracks.length)%tracks.length);}; window.aboutPlaylistTogglePlay=function(){playing=!playing;const playBtn=el('aboutPlaylistPlayBtn');if(playBtn)playBtn.textContent=playing?'⏸':'▶';if(playing){clearInterval(pInt);pInt=setInterval(()=>{prog+=.2;if(prog>=100){prog=0;window.aboutPlaylistNext();return;}const tr=tracks[cur],elapsed=Math.floor(tr.total*prog/100);if(el('aboutPlaylistProgressFill'))el('aboutPlaylistProgressFill').style.width=prog+'%';if(el('aboutPlaylistTimeNow'))el('aboutPlaylistTimeNow').textContent=`${Math.floor(elapsed/60)}:${String(elapsed%60).padStart(2,'0')}`;},200);}else clearInterval(pInt);}; window.aboutPlaylistSeek=function(e){const bar=el('aboutPlaylistProgressBar');if(!bar)return;const r=bar.getBoundingClientRect();prog=Math.max(0,Math.min(100,((e.clientX-r.left)/r.width)*100));if(el('aboutPlaylistProgressFill'))el('aboutPlaylistProgressFill').style.width=prog+'%';};updateUI(); })();
Digital Scrapbook

moments & memories

Late nights · 2024
"the best bots get written between 2am and the void"
🌙
Anime season · ongoing
"watching something just to make a bot about it. no regrets."
🔪
From Rio · 2024
"writing characters who love badly is an art form. i have mastered it."
🎵
Playlist building · always
"if it sounds like a villain's theme, it goes on every playlist i own."
🌹
Café hours · daydreaming
"the aesthetic is intentional. the obsession is a lifestyle choice."
(function(){ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; document.body.dataset.theme = localStorage.getItem('theme') || (prefersDark ? 'dark' : 'light'); window.addEventListener('madokkaThemeChange', e => { if (e.detail?.theme) document.body.dataset.theme = e.detail.theme; }); const cv=document.getElementById('aboutScrapbookWater'); if(cv){const cx=cv.getContext('2d');let t=0;const rs=()=>{cv.width=window.innerWidth;cv.height=window.innerHeight;};rs();window.addEventListener('resize',rs);(function draw(){cx.clearRect(0,0,cv.width,cv.height);const col=document.body.dataset.theme!=='light'?'200,133,74':'122,74,32';for(let i=0;i<6;i++){cx.beginPath();const a=24+i*9,f=.005+i*.001,s=.007+i*.003,y=(cv.height/6)*i;cx.moveTo(0,y);for(let x=0;x<=cv.width;x+=4)cx.lineTo(x,y+Math.sin(x*f+t*s+i)*a+Math.cos(x*f*.5+t*s*.65)*(a*.36));cx.strokeStyle=`rgba(${col},${.035+i*.006})`;cx.lineWidth=1.5;cx.stroke();}t++;requestAnimationFrame(draw);})();} document.getElementById('aboutScrapbookPage')?.classList.add('visible'); const obs=new IntersectionObserver(entries=>{entries.forEach(e=>{if(e.isIntersecting){e.target.classList.add('in');obs.unobserve(e.target);}});},{threshold:.06});document.querySelectorAll('#aboutScrapbookPage .rv').forEach(el=>obs.observe(el)); const track=document.getElementById('aboutScrapbookTrack'),dotsEl=document.getElementById('aboutScrapbookDots'),prevBtn=document.getElementById('aboutScrapbookPrev'),nextBtn=document.getElementById('aboutScrapbookNext');if(track&&dotsEl){const cards=Array.from(track.children),total=cards.length;let si=0;for(let i=0;igo(i));dotsEl.appendChild(d);}function go(idx){si=Math.max(0,Math.min(idx,total-1));const step=cards[0]?cards[0].offsetWidth+18:318;track.style.transform=`translateX(-${si*step}px)`;dotsEl.querySelectorAll('.s-dot').forEach((d,i)=>d.classList.toggle('on',i===si));}nextBtn?.addEventListener('click',()=>go(si+1));prevBtn?.addEventListener('click',()=>go(si-1));let swX=0;track.addEventListener('touchstart',e=>swX=e.touches[0].clientX,{passive:true});track.addEventListener('touchend',e=>{const d=swX-e.changedTouches[0].clientX;if(Math.abs(d)>50)go(d>0?si+1:si-1);});} })(); (function () { function initAboutScrapbookCarousel() { const track = document.getElementById('aboutScrapbookTrack'); const dotsEl = document.getElementById('aboutScrapbookDots'); const prevBtn = document.getElementById('aboutScrapbookPrev'); const nextBtn = document.getElementById('aboutScrapbookNext'); if (!track || !dotsEl || !prevBtn || !nextBtn) return; const cards = Array.from(track.children); const total = cards.length; let current = 0; if (!total) return; dotsEl.innerHTML = ''; cards.forEach((_, i) => { const dot = document.createElement('button'); dot.type = 'button'; dot.className = 's-dot' + (i === 0 ? ' on' : ''); dot.setAttribute('aria-label', 'Go to memory ' + (i + 1)); dot.addEventListener('click', function () { goToSlide(i); }); dotsEl.appendChild(dot); }); function getStep() { const card = cards[0]; if (!card) return 0; const style = window.getComputedStyle(track); const gap = parseFloat(style.columnGap || style.gap || 18) || 18; return card.getBoundingClientRect().width + gap; } function getMaxIndex() { const parentWidth = track.parentElement.clientWidth; const trackWidth = track.scrollWidth; if (trackWidth <= parentWidth) return 0; return Math.max(0, Math.ceil((trackWidth - parentWidth) / getStep())); } function goToSlide(index) { const maxIndex = getMaxIndex(); current = Math.max(0, Math.min(index, maxIndex)); const maxOffset = Math.max(0, track.scrollWidth - track.parentElement.clientWidth); const targetOffset = Math.min(current * getStep(), maxOffset); track.style.transform = 'translateX(-' + targetOffset + 'px)'; dotsEl.querySelectorAll('.s-dot').forEach((dot, i) => { dot.classList.toggle('on', i === current); }); } prevBtn.type = 'button'; nextBtn.type = 'button'; prevBtn.addEventListener('click', function () { goToSlide(current - 1); }); nextBtn.addEventListener('click', function () { goToSlide(current + 1); }); let startX = 0; track.addEventListener('touchstart', function (e) { startX = e.touches[0].clientX; }, { passive: true }); track.addEventListener('touchend', function (e) { const diff = startX - e.changedTouches[0].clientX; if (Math.abs(diff) > 50) { goToSlide(diff > 0 ? current + 1 : current - 1); } }); window.addEventListener('resize', function () { goToSlide(current); }); goToSlide(0); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initAboutScrapbookCarousel); } else { setTimeout(initAboutScrapbookCarousel, 100); } })();