HARMONY RIGS
(() => { const FONT_FAMILY = 'JonFont, Helvetica, Arial, sans-serif'; function renderTitle(wrapper) { const source = wrapper.querySelector('.jonfont-title-text'); if (!source) return; const title = source.textContent.trim(); wrapper.querySelector('svg')?.remove(); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); text.setAttribute('font-family', FONT_FAMILY); text.setAttribute('font-size', '300'); text.setAttribute('fill', '#35358c'); text.setAttribute('text-anchor', 'middle'); text.setAttribute('dominant-baseline', 'middle'); text.textContent = title; svg.appendChild(text); wrapper.appendChild(svg); const fit = () => { try { const box = text.getBBox(); const padX = Math.max(64, box.width * 0.035); const padY = 18; svg.setAttribute( 'viewBox', `${box.x - padX} ${box.y - padY} ${box.width + padX * 2} ${box.height + padY * 2}` ); text.setAttribute('x', box.x + box.width / 2); text.setAttribute('y', box.y + box.height / 2 + box.height * 0.03); } catch {} }; fit(); window.addEventListener('resize', fit); } const init = () => { document .querySelectorAll('.jonfont-title') .forEach(renderTitle); }; if (document.fonts?.ready) { document.fonts.ready.then(init); } else { window.addEventListener('load', init); } })();
(() => { const REPO = 'https://api.github.com/repos/J0onbo0n/Harmony-Rigs-Images/contents'; const META = [ { title: 'Alira and Snotz Rigs', link: '#alira-snotz-rigs' }, { title: 'Richard Rig', link: '#richard-rig' }, { title: 'Cat Rig', link: '#cat-rig' }, { title: 'Bert and Doug Rigs', link: '#bert-doug-rigs' } ]; const gallery = document.getElementById('hr-gallery'); if (!gallery) return; fetch(REPO) .then(r => r.json()) .then(files => { files .filter(f => /\.webp$/i.test(f.name)) .sort((a, b) => { const na = parseInt(a.name, 10); const nb = parseInt(b.name, 10); return na - nb; }) .slice(0, META.length) .forEach((file, i) => { const a = document.createElement('a'); a.className = 'hr-item'; a.href = META[i].link; const img = document.createElement('img'); img.src = file.download_url; img.loading = 'lazy'; img.alt = META[i].title; const cap = document.createElement('div'); cap.className = 'hr-caption'; cap.textContent = META[i].title; a.append(img, cap); gallery.appendChild(a); }); }) .catch(() => { /* fail silently so Carrd never blanks */ }); })();
(() => { document.addEventListener("click", e => { const btn = e.target.closest(".back-to-top-btn"); if (!btn) return; e.preventDefault(); window.scrollTo({ top: 0, behavior: "smooth" }); }); })();
ALIRA AND SNOTZ RIGS
(() => { const FONT_FAMILY = 'JonFont, Helvetica, Arial, sans-serif'; function renderTitle(wrapper) { const source = wrapper.querySelector('.jonfont-title-text'); if (!source) return; const title = source.textContent.trim(); wrapper.querySelector('svg')?.remove(); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); text.setAttribute('font-family', FONT_FAMILY); text.setAttribute('font-size', '300'); text.setAttribute('fill', '#35358c'); text.setAttribute('text-anchor', 'middle'); text.setAttribute('dominant-baseline', 'middle'); text.textContent = title; svg.appendChild(text); wrapper.appendChild(svg); const fit = () => { try { const box = text.getBBox(); const padX = Math.max(64, box.width * 0.035); const padY = 18; svg.setAttribute( 'viewBox', `${box.x - padX} ${box.y - padY} ${box.width + padX * 2} ${box.height + padY * 2}` ); text.setAttribute('x', box.x + box.width / 2); text.setAttribute('y', box.y + box.height / 2 + box.height * 0.03); } catch {} }; fit(); window.addEventListener('resize', fit); } const init = () => { document .querySelectorAll('.jonfont-title') .forEach(renderTitle); }; if (document.fonts?.ready) { document.fonts.ready.then(init); } else { window.addEventListener('load', init); } })();
Alira 360° Rig Showcase
Snotz 360° Rig Showcase
RICHARD RIG
(() => { const FONT_FAMILY = 'JonFont, Helvetica, Arial, sans-serif'; function renderTitle(wrapper) { const source = wrapper.querySelector('.jonfont-title-text'); if (!source) return; const title = source.textContent.trim(); wrapper.querySelector('svg')?.remove(); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); text.setAttribute('font-family', FONT_FAMILY); text.setAttribute('font-size', '300'); text.setAttribute('fill', '#35358c'); text.setAttribute('text-anchor', 'middle'); text.setAttribute('dominant-baseline', 'middle'); text.textContent = title; svg.appendChild(text); wrapper.appendChild(svg); const fit = () => { try { const box = text.getBBox(); const padX = Math.max(64, box.width * 0.035); const padY = 18; svg.setAttribute( 'viewBox', `${box.x - padX} ${box.y - padY} ${box.width + padX * 2} ${box.height + padY * 2}` ); text.setAttribute('x', box.x + box.width / 2); text.setAttribute('y', box.y + box.height / 2 + box.height * 0.03); } catch {} }; fit(); window.addEventListener('resize', fit); } const init = () => { document .querySelectorAll('.jonfont-title') .forEach(renderTitle); }; if (document.fonts?.ready) { document.fonts.ready.then(init); } else { window.addEventListener('load', init); } })();
Richard 270° Rig Showcase
CAT RIG
(() => { const FONT_FAMILY = 'JonFont, Helvetica, Arial, sans-serif'; function renderTitle(wrapper) { const source = wrapper.querySelector('.jonfont-title-text'); if (!source) return; const title = source.textContent.trim(); wrapper.querySelector('svg')?.remove(); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); text.setAttribute('font-family', FONT_FAMILY); text.setAttribute('font-size', '300'); text.setAttribute('fill', '#35358c'); text.setAttribute('text-anchor', 'middle'); text.setAttribute('dominant-baseline', 'middle'); text.textContent = title; svg.appendChild(text); wrapper.appendChild(svg); const fit = () => { try { const box = text.getBBox(); const padX = Math.max(64, box.width * 0.035); const padY = 18; svg.setAttribute( 'viewBox', `${box.x - padX} ${box.y - padY} ${box.width + padX * 2} ${box.height + padY * 2}` ); text.setAttribute('x', box.x + box.width / 2); text.setAttribute('y', box.y + box.height / 2 + box.height * 0.03); } catch {} }; fit(); window.addEventListener('resize', fit); } const init = () => { document .querySelectorAll('.jonfont-title') .forEach(renderTitle); }; if (document.fonts?.ready) { document.fonts.ready.then(init); } else { window.addEventListener('load', init); } })();
Cat Rig (180° Head) Showcase
BERT AND DOUG RIGS
(() => { const FONT_FAMILY = 'JonFont, Helvetica, Arial, sans-serif'; function renderTitle(wrapper) { const source = wrapper.querySelector('.jonfont-title-text'); if (!source) return; const title = source.textContent.trim(); wrapper.querySelector('svg')?.remove(); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); text.setAttribute('font-family', FONT_FAMILY); text.setAttribute('font-size', '300'); text.setAttribute('fill', '#35358c'); text.setAttribute('text-anchor', 'middle'); text.setAttribute('dominant-baseline', 'middle'); text.textContent = title; svg.appendChild(text); wrapper.appendChild(svg); const fit = () => { try { const box = text.getBBox(); const padX = Math.max(64, box.width * 0.035); const padY = 18; svg.setAttribute( 'viewBox', `${box.x - padX} ${box.y - padY} ${box.width + padX * 2} ${box.height + padY * 2}` ); text.setAttribute('x', box.x + box.width / 2); text.setAttribute('y', box.y + box.height / 2 + box.height * 0.03); } catch {} }; fit(); window.addEventListener('resize', fit); } const init = () => { document .querySelectorAll('.jonfont-title') .forEach(renderTitle); }; if (document.fonts?.ready) { document.fonts.ready.then(init); } else { window.addEventListener('load', init); } })();
Doug 180° Rig Showcase
Bert Rig Showcase
DEMO REEL
(() => { const FONT_FAMILY = 'JonFont, Helvetica, Arial, sans-serif'; function renderTitle(wrapper) { const source = wrapper.querySelector('.jonfont-title-text'); if (!source) return; const title = source.textContent.trim(); wrapper.querySelector('svg')?.remove(); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); text.setAttribute('font-family', FONT_FAMILY); text.setAttribute('font-size', '300'); text.setAttribute('fill', '#35358c'); text.setAttribute('text-anchor', 'middle'); text.setAttribute('dominant-baseline', 'middle'); text.textContent = title; svg.appendChild(text); wrapper.appendChild(svg); const fit = () => { try { const box = text.getBBox(); const padX = Math.max(64, box.width * 0.035); const padY = 18; svg.setAttribute( 'viewBox', `${box.x - padX} ${box.y - padY} ${box.width + padX * 2} ${box.height + padY * 2}` ); text.setAttribute('x', box.x + box.width / 2); text.setAttribute('y', box.y + box.height / 2 + box.height * 0.03); } catch {} }; fit(); window.addEventListener('resize', fit); } const init = () => { document .querySelectorAll('.jonfont-title') .forEach(renderTitle); }; if (document.fonts?.ready) { document.fonts.ready.then(init); } else { window.addEventListener('load', init); } })();
Video
THESIS FILM
(() => { const FONT_FAMILY = 'JonFont, Helvetica, Arial, sans-serif'; function renderTitle(wrapper) { const source = wrapper.querySelector('.jonfont-title-text'); if (!source) return; const title = source.textContent.trim(); wrapper.querySelector('svg')?.remove(); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); text.setAttribute('font-family', FONT_FAMILY); text.setAttribute('font-size', '300'); text.setAttribute('fill', '#35358c'); text.setAttribute('text-anchor', 'middle'); text.setAttribute('dominant-baseline', 'middle'); text.textContent = title; svg.appendChild(text); wrapper.appendChild(svg); const fit = () => { try { const box = text.getBBox(); const padX = Math.max(64, box.width * 0.035); const padY = 18; svg.setAttribute( 'viewBox', `${box.x - padX} ${box.y - padY} ${box.width + padX * 2} ${box.height + padY * 2}` ); text.setAttribute('x', box.x + box.width / 2); text.setAttribute('y', box.y + box.height / 2 + box.height * 0.03); } catch {} }; fit(); window.addEventListener('resize', fit); } const init = () => { document .querySelectorAll('.jonfont-title') .forEach(renderTitle); }; if (document.fonts?.ready) { document.fonts.ready.then(init); } else { window.addEventListener('load', init); } })();
In my 4th year in Sheridan College's Bachelor of Animation Program, I was tasked with making an animated thesis film. Here is the final film, as well as some behind the scenes.

Here are the various painted backgrounds/stills from the film:
(() => { const root = document.getElementById('tb-gallery-2'); const viewerImg = root.querySelector('.tl-frame img'); const prevBtn = root.querySelector('.tl-arrow.left'); const nextBtn = root.querySelector('.tl-arrow.right'); const lightbox = root.querySelector('.tl-lightbox'); const lbImg = root.querySelector('.tl-lightbox img'); const layer = root.querySelector('.tl-layer'); const wrap = root.querySelector('.tl-wrap'); const closeBtn = root.querySelector('.tl-close'); const lbNext = root.querySelector('.tl-next'); const lbPrev = root.querySelector('.tl-prev'); const isMobile = () => matchMedia('(max-width: 768px)').matches; const REPO = 'https://api.github.com/repos/J0onbo0n/Thesis-Layouts/contents'; let images = []; let index = 0; fetch(REPO) .then(r => r.json()) .then(files => { images = files .filter(f => /\.(webp|png|jpg|jpeg)$/i.test(f.name)) .sort((a,b)=>a.name.localeCompare(b.name,undefined,{numeric:true})); if (!images.length) return; update(); }); function update() { viewerImg.src = images[index].download_url; } function loadLightboxImage() { lbImg.onload = () => fit(); lbImg.src = images[index].download_url; } /* NAV (UNCHANGED) */ nextBtn.onclick = () => { index = (index + 1) % images.length; update(); }; prevBtn.onclick = () => { index = (index - 1 + images.length) % images.length; update(); }; /* ✅ FIXED MOBILE TAP NAV (SAFE - NO OVERLAYS) */ root.querySelector('.tl-viewer').addEventListener('click', (e) => { if (!isMobile()) return; if (e.target.closest('.tl-arrow')) return; const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; if (x < rect.width / 2) { index = (index - 1 + images.length) % images.length; } else { index = (index + 1) % images.length; } update(); }); /* LIGHTBOX (UNCHANGED EXACTLY) */ let zoom = 1, base = 1, panX = 0, panY = 0; let dragging = false, sx = 0, sy = 0; function apply() { layer.style.transform = `translate3d(${panX}px, ${panY}px, 0) scale(${zoom * base})`; } function fit() { if (!wrap.clientWidth || !lbImg.naturalWidth) return; const cs = getComputedStyle(root); const pad = parseInt(cs.getPropertyValue('--safe-pad')); const gx = parseInt(cs.getPropertyValue('--ui-gutter-x')); const gy = parseInt(cs.getPropertyValue('--ui-gutter-top')); const fw = wrap.clientWidth - gx * 2 - pad * 2; const fh = wrap.clientHeight - gy - pad * 2; base = Math.min(fw / lbImg.naturalWidth, fh / lbImg.naturalHeight) * 1.15; panX = (wrap.clientWidth - lbImg.naturalWidth * base) / 2; panY = (wrap.clientHeight - lbImg.naturalHeight * base) / 2 + gy * 0.25; zoom = 1; apply(); } function open() { if (isMobile()) return; loadLightboxImage(); lightbox.classList.add('active'); document.body.style.overflow = 'hidden'; } function close() { lightbox.classList.remove('active'); document.body.style.overflow = ''; update(); } viewerImg.onclick = open; closeBtn.onclick = close; lbNext.onclick = () => { index = (index + 1) % images.length; update(); loadLightboxImage(); }; lbPrev.onclick = () => { index = (index - 1 + images.length) % images.length; update(); loadLightboxImage(); }; wrap.addEventListener('wheel', e => { e.preventDefault(); const r = lbImg.getBoundingClientRect(); const ox = (e.clientX - r.left) / (zoom * base); const oy = (e.clientY - r.top) / (zoom * base); const prevZoom = zoom; zoom = Math.min(Math.max(1, zoom + e.deltaY * -0.001), 2.5); if (zoom === 1) return fit(); panX -= ox * (zoom - prevZoom) * base; panY -= oy * (zoom - prevZoom) * base; apply(); },{passive:false}); lbImg.addEventListener('pointerdown', e => { if (zoom <= 1) return; dragging = true; sx = e.clientX - panX; sy = e.clientY - panY; }); window.addEventListener('pointermove', e => { if (!dragging) return; panX = e.clientX - sx; panY = e.clientY - sy; apply(); }); window.addEventListener('pointerup', () => { dragging = false; }); document.addEventListener('keydown', e => { if (e.key === 'Escape') close(); }); })();

Here are the storyboards from the film:
(() => { const root = document.getElementById('tb-gallery'); const viewerImg = root.querySelector('.tl-frame img'); const prevBtn = root.querySelector('.tl-arrow.left'); const nextBtn = root.querySelector('.tl-arrow.right'); const lightbox = root.querySelector('.tl-lightbox'); const lbImg = root.querySelector('.tl-lightbox img'); const layer = root.querySelector('.tl-layer'); const wrap = root.querySelector('.tl-wrap'); const closeBtn = root.querySelector('.tl-close'); const lbNext = root.querySelector('.tl-next'); const lbPrev = root.querySelector('.tl-prev'); const isMobile = () => matchMedia('(max-width: 768px)').matches; const REPO = 'https://api.github.com/repos/J0onbo0n/Thesis-Boards/contents'; let images = []; let index = 0; fetch(REPO) .then(r => r.json()) .then(files => { images = files .filter(f => /\.(webp|png|jpg|jpeg)$/i.test(f.name)) .sort((a,b)=>a.name.localeCompare(b.name,undefined,{numeric:true})); if (!images.length) return; update(); }); function update() { viewerImg.src = images[index].download_url; } function loadLightboxImage() { lbImg.onload = () => fit(); lbImg.src = images[index].download_url; } /* NAV */ nextBtn.onclick = () => { index = (index + 1) % images.length; update(); }; prevBtn.onclick = () => { index = (index - 1 + images.length) % images.length; update(); }; /* MOBILE TAP NAV */ root.querySelector('.tl-viewer').addEventListener('click', (e) => { if (!isMobile()) return; if (e.target.closest('.tl-arrow')) return; const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; if (x < rect.width / 2) { index = (index - 1 + images.length) % images.length; } else { index = (index + 1) % images.length; } update(); }); /* LIGHTBOX */ function open() { if (isMobile()) return; loadLightboxImage(); lightbox.classList.add('active'); document.body.style.overflow = 'hidden'; } function close() { lightbox.classList.remove('active'); document.body.style.overflow = ''; update(); } viewerImg.onclick = open; closeBtn.onclick = close; lbNext.onclick = () => { index = (index + 1) % images.length; update(); loadLightboxImage(); }; lbPrev.onclick = () => { index = (index - 1 + images.length) % images.length; update(); loadLightboxImage(); }; /* ZOOM */ let zoom = 1, base = 1, panX = 0, panY = 0; let dragging = false, sx = 0, sy = 0; function apply() { layer.style.transform = `translate3d(${panX}px, ${panY}px, 0) scale(${zoom * base})`; } function fit() { if (!wrap.clientWidth || !lbImg.naturalWidth) return; const cs = getComputedStyle(root); const pad = parseInt(cs.getPropertyValue('--safe-pad')); const gx = parseInt(cs.getPropertyValue('--ui-gutter-x')); const gy = parseInt(cs.getPropertyValue('--ui-gutter-top')); const fw = wrap.clientWidth - gx * 2 - pad * 2; const fh = wrap.clientHeight - gy - pad * 2; base = Math.min(fw / lbImg.naturalWidth, fh / lbImg.naturalHeight) * 1.15; panX = (wrap.clientWidth - lbImg.naturalWidth * base) / 2; panY = (wrap.clientHeight - lbImg.naturalHeight * base) / 2 + gy * 0.25; zoom = 1; apply(); } wrap.addEventListener('wheel', e => { e.preventDefault(); const r = lbImg.getBoundingClientRect(); const ox = (e.clientX - r.left) / (zoom * base); const oy = (e.clientY - r.top) / (zoom * base); const prevZoom = zoom; zoom = Math.min(Math.max(1, zoom + e.deltaY * -0.001), 2.5); if (zoom === 1) return fit(); panX -= ox * (zoom - prevZoom) * base; panY -= oy * (zoom - prevZoom) * base; apply(); },{passive:false}); lbImg.addEventListener('pointerdown', e => { if (zoom <= 1) return; dragging = true; sx = e.clientX - panX; sy = e.clientY - panY; }); window.addEventListener('pointermove', e => { if (!dragging) return; panX = e.clientX - sx; panY = e.clientY - sy; apply(); }); window.addEventListener('pointerup', () => dragging = false); document.addEventListener('keydown', e => { if (e.key === 'Escape') close(); }); })();

Here are various concept art pieces from the film:
×
(() => { const ROOT = document.getElementById('jb3-root'); const gallery = ROOT.querySelector('#jb3-gallery'); const REPO = 'https://api.github.com/repos/J0onbo0n/Thesis-Concept-Art/contents'; let rowHeight, gap; function measure() { const cs = getComputedStyle(gallery); rowHeight = parseInt(cs.gridAutoRows); gap = parseInt(cs.rowGap); } function spanFor(h) { return Math.ceil((h + gap) / (rowHeight + gap)); } function layoutItem(item) { const el = item.classList.contains('loaded') ? item.querySelector('img') : item.querySelector('.jb3-ph'); const h = el.getBoundingClientRect().height; if (h) item.style.gridRowEnd = `span ${spanFor(h)}`; } fetch(REPO) .then(r => r.json()) .then(files => { files .filter(f => /\.webp$/i.test(f.name)) .sort((a,b)=>a.name.localeCompare(b.name,undefined,{numeric:true})) .forEach(file => { const item = document.createElement('div'); item.className = 'jb3-item'; const ph = document.createElement('div'); ph.className = 'jb3-ph'; const img = document.createElement('img'); img.src = file.download_url; item.append(ph, img); gallery.appendChild(item); measure(); layoutItem(item); img.onload = () => { const ar = img.naturalWidth / img.naturalHeight; ph.style.aspectRatio = ar; item.style.setProperty('--ar', ar); if (ar > 16/9) item.classList.add('wide'); measure(); layoutItem(item); requestAnimationFrame(() => { item.classList.add('loaded'); layoutItem(item); }); }; new ResizeObserver(() => layoutItem(item)).observe(item); }); }); const lightbox = ROOT.querySelector('#jb3-lightbox'); const img = ROOT.querySelector('#jb3-lightbox-img'); const layer = ROOT.querySelector('#jb3-layer'); const wrap = ROOT.querySelector('#jb3-wrap'); const closeBtn = ROOT.querySelector('#jb3-close'); const nextBtn = ROOT.querySelector('#jb3-next'); const prevBtn = ROOT.querySelector('#jb3-prev'); let index = 0, zoom = 1, base = 1, panX = 0, panY = 0; let dragging = false, sx = 0, sy = 0; const thumbs = () => [...ROOT.querySelectorAll('.jb3-item img')]; const isMobile = () => matchMedia('(max-width: 768px)').matches; function apply() { layer.style.transform = `translate3d(${panX}px, ${panY}px, 0) scale(${zoom * base})`; } function fit() { if (!wrap.clientWidth || !img.naturalWidth) return; const cs = getComputedStyle(document.documentElement); const pad = parseInt(cs.getPropertyValue('--safe-pad')); const gx = parseInt(cs.getPropertyValue('--ui-gutter-x')); const gy = parseInt(cs.getPropertyValue('--ui-gutter-top')); const fw = wrap.clientWidth - gx * 2 - pad * 2; const fh = wrap.clientHeight - gy - pad * 2; base = Math.min(fw / img.naturalWidth, fh / img.naturalHeight) * 1.15; panX = (wrap.clientWidth - img.naturalWidth * base) / 2; panY = (wrap.clientHeight - img.naturalHeight * base) / 2 + gy * 0.25; zoom = 1; apply(); } function openAt(i) { if (isMobile()) return; const t = thumbs(); if (!t[i]) return; index = i; img.src = t[i].src; lightbox.classList.add('active'); document.body.style.overflow = 'hidden'; img.onload = fit; requestAnimationFrame(fit); } function close() { lightbox.classList.remove('active'); document.body.style.overflow = ''; } ROOT.addEventListener('pointerup', e => { if (isMobile()) return; const el = e.target.closest('.jb3-item img'); if (!el) return; const i = thumbs().indexOf(el); if (i !== -1) openAt(i); }, true); closeBtn.onclick = close; nextBtn.onclick = () => openAt((index + 1) % thumbs().length); prevBtn.onclick = () => openAt((index - 1 + thumbs().length) % thumbs().length); /* 🔥 REDUCED MAX ZOOM */ wrap.addEventListener('wheel', e => { e.preventDefault(); const r = img.getBoundingClientRect(); const ox = (e.clientX - r.left) / (zoom * base); const oy = (e.clientY - r.top) / (zoom * base); const p = zoom; zoom = Math.min(Math.max(1, zoom + e.deltaY * -0.001), 2.25); if (zoom === 1) return fit(); panX -= ox * (zoom - p) * base; panY -= oy * (zoom - p) * base; apply(); },{passive:false}); img.addEventListener('pointerdown', e => { if (zoom <= 1) return; dragging = true; sx = e.clientX - panX; sy = e.clientY - panY; img.classList.add('dragging'); }); window.addEventListener('pointermove', e => { if (!dragging) return; panX = e.clientX - sx; panY = e.clientY - sy; apply(); }); window.addEventListener('pointerup', () => { dragging = false; img.classList.remove('dragging'); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') close(); }); })();
(() => { document.addEventListener("click", e => { const btn = e.target.closest(".back-to-top-btn"); if (!btn) return; e.preventDefault(); window.scrollTo({ top: 0, behavior: "smooth" }); }); })();
FIGURE DRAWING
(() => { const FONT_FAMILY = 'JonFont, Helvetica, Arial, sans-serif'; function renderTitle(wrapper) { const source = wrapper.querySelector('.jonfont-title-text'); if (!source) return; const title = source.textContent.trim(); wrapper.querySelector('svg')?.remove(); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); text.setAttribute('font-family', FONT_FAMILY); text.setAttribute('font-size', '300'); text.setAttribute('fill', '#35358c'); text.setAttribute('text-anchor', 'middle'); text.setAttribute('dominant-baseline', 'middle'); text.textContent = title; svg.appendChild(text); wrapper.appendChild(svg); const fit = () => { try { const box = text.getBBox(); const padX = Math.max(64, box.width * 0.035); const padY = 18; svg.setAttribute( 'viewBox', `${box.x - padX} ${box.y - padY} ${box.width + padX * 2} ${box.height + padY * 2}` ); text.setAttribute('x', box.x + box.width / 2); text.setAttribute('y', box.y + box.height / 2 + box.height * 0.03); } catch {} }; fit(); window.addEventListener('resize', fit); } const init = () => { document .querySelectorAll('.jonfont-title') .forEach(renderTitle); }; if (document.fonts?.ready) { document.fonts.ready.then(init); } else { window.addEventListener('load', init); } })();
(() => { const REPO = 'https://api.github.com/repos/J0onbo0n/FigureDrawing-Images/contents'; const gallery = document.getElementById('gallery'); let rowHeight, gap; function measure() { const cs = getComputedStyle(gallery); rowHeight = parseInt(cs.gridAutoRows); gap = parseInt(cs.rowGap); } function spanFor(h) { return Math.ceil((h + gap) / (rowHeight + gap)); } function layoutItem(item) { const el = item.classList.contains('loaded') ? item.querySelector('img') : item.querySelector('.jb-ph'); const h = el.getBoundingClientRect().height; if (h) item.style.gridRowEnd = `span ${spanFor(h)}`; } fetch(REPO) .then(r => r.json()) .then(files => { files .filter(f => /\.webp$/i.test(f.name)) .sort((a,b)=>a.name.localeCompare(b.name,undefined,{numeric:true})) .forEach(file => { const item = document.createElement('div'); item.className = 'jb-item'; const ph = document.createElement('div'); ph.className = 'jb-ph'; const img = document.createElement('img'); img.src = file.download_url; item.append(ph, img); gallery.appendChild(item); measure(); layoutItem(item); img.onload = () => { const ar = img.naturalWidth / img.naturalHeight; ph.style.aspectRatio = ar; item.style.setProperty('--ar', ar); if (ar > 16/9) item.classList.add('wide'); measure(); layoutItem(item); requestAnimationFrame(() => { item.classList.add('loaded'); layoutItem(item); }); }; new ResizeObserver(() => layoutItem(item)).observe(item); }); }); /* LIGHTBOX — unchanged */ const lightbox = document.getElementById('lightbox'); const img = document.getElementById('lightbox-img'); const layer = document.getElementById('layer'); const wrap = document.getElementById('wrap'); const closeBtn = document.getElementById('close'); const nextBtn = document.getElementById('next'); const prevBtn = document.getElementById('prev'); let index = 0, zoom = 1, base = 1, panX = 0, panY = 0; let dragging = false, sx = 0, sy = 0; const thumbs = () => [...document.querySelectorAll('.jb-item img')]; const isMobile = () => matchMedia('(max-width: 768px)').matches; function apply() { layer.style.transform = `translate3d(${panX}px, ${panY}px, 0) scale(${zoom * base})`; } function fit() { if (!wrap.clientWidth || !img.naturalWidth) return; const cs = getComputedStyle(document.documentElement); const pad = parseInt(cs.getPropertyValue('--safe-pad')); const gx = parseInt(cs.getPropertyValue('--ui-gutter-x')); const gy = parseInt(cs.getPropertyValue('--ui-gutter-top')); const fw = wrap.clientWidth - gx * 2 - pad * 2; const fh = wrap.clientHeight - gy - pad * 2; base = Math.min(fw / img.naturalWidth, fh / img.naturalHeight) * 1.15; panX = (wrap.clientWidth - img.naturalWidth * base) / 2; panY = (wrap.clientHeight - img.naturalHeight * base) / 2 + gy * 0.25; zoom = 1; apply(); } function openAt(i) { const t = thumbs(); if (!t[i]) return; index = i; img.src = t[i].src; lightbox.classList.add('active'); document.body.style.overflow = 'hidden'; img.onload = fit; requestAnimationFrame(fit); } function close() { lightbox.classList.remove('active'); document.body.style.overflow = ''; } document.addEventListener('pointerup', e => { if (isMobile()) return; const el = e.target.closest('.jb-item img'); if (!el) return; const i = thumbs().indexOf(el); if (i !== -1) openAt(i); }, true); closeBtn.onclick = close; nextBtn.onclick = () => openAt((index + 1) % thumbs().length); prevBtn.onclick = () => openAt((index - 1 + thumbs().length) % thumbs().length); wrap.addEventListener('wheel', e => { e.preventDefault(); const r = img.getBoundingClientRect(); const ox = (e.clientX - r.left) / (zoom * base); const oy = (e.clientY - r.top) / (zoom * base); const p = zoom; zoom = Math.min(Math.max(1, zoom + e.deltaY * -0.001), 4.5); if (zoom === 1) return fit(); panX -= ox * (zoom - p) * base; panY -= oy * (zoom - p) * base; apply(); },{passive:false}); img.addEventListener('pointerdown', e => { if (zoom <= 1) return; dragging = true; sx = e.clientX - panX; sy = e.clientY - panY; img.classList.add('dragging'); }); window.addEventListener('pointermove', e => { if (!dragging) return; panX = e.clientX - sx; panY = e.clientY - sy; apply(); }); window.addEventListener('pointerup', () => { dragging = false; img.classList.remove('dragging'); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') close(); }); })();
(() => { document.addEventListener("click", e => { const btn = e.target.closest(".back-to-top-btn"); if (!btn) return; e.preventDefault(); window.scrollTo({ top: 0, behavior: "smooth" }); }); })();
ABOUT ME
(() => { const FONT_FAMILY = 'JonFont, Helvetica, Arial, sans-serif'; function renderTitle(wrapper) { const source = wrapper.querySelector('.jonfont-title-text'); if (!source) return; const title = source.textContent.trim(); wrapper.querySelector('svg')?.remove(); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); text.setAttribute('font-family', FONT_FAMILY); text.setAttribute('font-size', '300'); text.setAttribute('fill', '#35358c'); text.setAttribute('text-anchor', 'middle'); text.setAttribute('dominant-baseline', 'middle'); text.textContent = title; svg.appendChild(text); wrapper.appendChild(svg); const fit = () => { try { const box = text.getBBox(); const padX = Math.max(64, box.width * 0.035); const padY = 18; svg.setAttribute( 'viewBox', `${box.x - padX} ${box.y - padY} ${box.width + padX * 2} ${box.height + padY * 2}` ); text.setAttribute('x', box.x + box.width / 2); text.setAttribute('y', box.y + box.height / 2 + box.height * 0.03); } catch {} }; fit(); window.addEventListener('resize', fit); } const init = () => { document .querySelectorAll('.jonfont-title') .forEach(renderTitle); }; if (document.fonts?.ready) { document.fonts.ready.then(init); } else { window.addEventListener('load', init); } })();
Hello! My name is Jonathan Bennett, I am a 2D Rigging/Builds Artist and Animator using ToonBoom Harmony to bring characters to life. I have always had a passion for art and problem solving, and am always striving to learn and share my knowledge with others. Let's help each other grow!

To get in touch with me, reach out to [email protected]