/* Ask Sewphie — vanilla JS, no build step */ (function () { "use strict"; /* ---------------------------------------------------------------- Monetisation config — fill in to activate. Empty string = off. - AMAZON_TAG_UK: from https://affiliate-program.amazon.co.uk (looks like "asksewphie-21") - KOFI_USERNAME: from https://ko-fi.com (e.g. "asksewphie") ---------------------------------------------------------------- */ const AMAZON_TAG_UK = "ivcorp-21"; const KOFI_USERNAME = "coffee4moi"; /* ---------- Routing (History API, real URLs per tool) ---------- */ const SITE_URL = "https://asksewphie.com"; const ROUTE_META = { home: { path: "/", title: "Ask Sewphie — Sewing calculators & reference", desc: "Friendly sewing calculators and reference charts: fabric yardage, inch/cm, bias binding, buttonhole spacing, needle, thread, stitch and care guides." }, yardage: { path: "/yardage", title: "Fabric yardage calculator — Ask Sewphie", desc: "Calculate how much fabric you need from piece dimensions, fabric width, shrinkage and seam allowance. Live results in yards or metres." }, convert: { path: "/convert", title: "Inch to cm converter (with fractions) — Ask Sewphie", desc: "Convert inches to centimetres and back, including fractional inches like 5/8 or 1 1/2. Live two-way converter for sewing measurements." }, bias: { path: "/bias", title: "Bias binding calculator — Ask Sewphie", desc: "Work out the square size of fabric to cut for a given total length and finished width of bias binding." }, buttons: { path: "/buttons", title: "Buttonhole spacing calculator — Ask Sewphie", desc: "Calculate evenly spaced buttonhole positions along a placket from total length, top and bottom offsets, and button count." }, needles: { path: "/needles", title: "Sewing machine needle chart — Ask Sewphie", desc: "Reference chart of sewing machine needle types and sizes — Universal, Ballpoint, Stretch, Jeans, Microtex, Leather — with recommended fabrics." }, thread: { path: "/thread", title: "Thread weight chart (Wt, Tex, Denier) — Ask Sewphie", desc: "Sewing thread weight reference: Wt, Tex and Denier compared, with typical uses for each weight from heavy topstitch to fine bobbin." }, stitches: { path: "/stitches", title: "Stitch length reference — Ask Sewphie", desc: "Recommended stitch lengths for basting, construction, topstitching, zigzag, buttonholes and more." }, care: { path: "/care", title: "Fabric care symbols (ISO 3758) — Ask Sewphie", desc: "Reference of ISO 3758 fabric care label symbols — washing, bleaching, drying, ironing and dry cleaning — with plain-English descriptions." }, privacy: { path: "/privacy", title: "Privacy — Ask Sewphie", desc: "What data Ask Sewphie collects, when, and why. No accounts, no ads." } }; const ROUTES = Object.keys(ROUTE_META); const backBtn = document.getElementById("back-btn"); const main = document.getElementById("main"); function setMetaName(name, value) { let el = document.querySelector('meta[name="' + name + '"]'); if (!el) { el = document.createElement("meta"); el.setAttribute("name", name); document.head.appendChild(el); } el.setAttribute("content", value); } function setMetaProperty(prop, value) { let el = document.querySelector('meta[property="' + prop + '"]'); if (!el) { el = document.createElement("meta"); el.setAttribute("property", prop); document.head.appendChild(el); } el.setAttribute("content", value); } function setCanonical(href) { let el = document.querySelector('link[rel="canonical"]'); if (!el) { el = document.createElement("link"); el.setAttribute("rel", "canonical"); document.head.appendChild(el); } el.setAttribute("href", href); } function applyRouteMeta(route) { const m = ROUTE_META[route] || ROUTE_META.home; const url = SITE_URL + m.path; document.title = m.title; setMetaName("description", m.desc); setCanonical(url); setMetaProperty("og:title", m.title); setMetaProperty("og:description", m.desc); setMetaProperty("og:url", url); setMetaName("twitter:title", m.title); setMetaName("twitter:description", m.desc); } function show(route) { if (!ROUTES.includes(route)) route = "home"; document.querySelectorAll(".view").forEach(v => { v.hidden = true; v.classList.remove("view-active"); }); const target = document.getElementById("view-" + route); if (target) { target.hidden = false; void target.offsetWidth; target.classList.add("view-active"); } backBtn.hidden = (route === "home"); applyRouteMeta(route); main.focus({ preventScroll: true }); window.scrollTo({ top: 0, behavior: "instant" in window ? "instant" : "auto" }); } function routeFromPath() { const p = (location.pathname || "/").replace(/\/+$/, "") || "/"; for (const r of ROUTES) { if (ROUTE_META[r].path === p || (p === "" && r === "home")) return r; } return "home"; } function navigate(route) { const m = ROUTE_META[route] || ROUTE_META.home; if (location.pathname !== m.path) history.pushState(null, "", m.path); show(route); } document.addEventListener("click", (e) => { const el = e.target.closest("[data-route]"); if (!el) return; if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return; e.preventDefault(); navigate(el.getAttribute("data-route")); }); backBtn.addEventListener("click", (e) => { e.preventDefault(); navigate("home"); }); window.addEventListener("popstate", () => show(routeFromPath())); /* ---------- Helpers ---------- */ const $ = (id) => document.getElementById(id); const round = (n, d=2) => { const f = Math.pow(10, d); return Math.round(n * f) / f; }; function nearestEighth(decInches) { if (!isFinite(decInches)) return ""; const sign = decInches < 0 ? "-" : ""; decInches = Math.abs(decInches); const whole = Math.floor(decInches); const eighths = Math.round((decInches - whole) * 8); if (eighths === 0) return sign + whole + "″"; if (eighths === 8) return sign + (whole + 1) + "″"; // simplify let num = eighths, den = 8; while (num % 2 === 0) { num /= 2; den /= 2; } return sign + (whole > 0 ? whole + " " : "") + num + "/" + den + "″"; } function parseInches(str) { if (str == null) return NaN; const s = String(str).trim().replace(/[″"]/g, "").replace(/-/g, " "); if (!s) return NaN; // pure decimal if (/^-?\d+(\.\d+)?$/.test(s)) return parseFloat(s); // fraction with optional whole: "1 1/2" or "1/2" const m = s.match(/^(-?)(?:(\d+)\s+)?(\d+)\s*\/\s*(\d+)$/); if (m) { const sign = m[1] === "-" ? -1 : 1; const whole = m[2] ? parseInt(m[2], 10) : 0; const num = parseInt(m[3], 10); const den = parseInt(m[4], 10); if (den === 0) return NaN; return sign * (whole + num / den); } return NaN; } /* ---------- Yardage ---------- */ function calcYardage() { const units = $("y-units").value; const len = parseFloat($("y-piece-len").value); const wid = parseFloat($("y-piece-wid").value); const qty = Math.max(1, parseInt($("y-qty").value, 10) || 1); const fwid = parseFloat($("y-fabric-wid").value); const shrink = (parseFloat($("y-shrink").value) || 0) / 100; const seam = (parseFloat($("y-seam").value) || 0) / 100; const out = $("y-result"); if (![len, wid, fwid].every(v => isFinite(v) && v > 0)) { out.className = "result"; out.innerHTML = "

Enter values

Fill in piece length, piece width, and fabric width to see an estimate.

"; return; } if (wid > fwid) { out.className = "result warn"; out.innerHTML = `

Piece is wider than the fabric

Your piece width (${wid} ${units}) is larger than the fabric width (${fwid} ${units}). Either rotate the piece, choose wider fabric, or piece together panels.

`; return; } const piecesPerRow = Math.max(1, Math.floor(fwid / wid)); const rows = Math.ceil(qty / piecesPerRow); const baseLen = rows * len; const totalLen = baseLen * (1 + shrink) * (1 + seam); const yards = totalLen / 36; const meters = totalLen / 100; const primary = units === "in" ? `${round(yards, 2)} yards (${round(totalLen,1)} in)` : `${round(meters, 2)} m (${round(totalLen,1)} cm)`; out.className = "result"; out.innerHTML = `

Buy at least

${primary}
`; } ["y-units","y-piece-len","y-piece-wid","y-qty","y-fabric-wid","y-shrink","y-seam"].forEach(id => { const el = $(id); if (el) el.addEventListener("input", calcYardage); }); /* ---------- Convert ---------- */ const cInch = $("c-inch"); const cCm = $("c-cm"); const cInchFrac = $("c-inch-frac"); const cMm = $("c-mm"); let convertLock = false; function fromInch() { if (convertLock) return; const v = parseInches(cInch.value); if (!isFinite(v)) { cCm.value = ""; cInchFrac.textContent = cInch.value ? "Try formats like 5/8, 1.5, or 1 1/2" : " "; cMm.textContent = " "; return; } convertLock = true; cCm.value = round(v * 2.54, 3).toString(); convertLock = false; cInchFrac.textContent = `≈ ${nearestEighth(v)} (decimal ${round(v,3)})`; cMm.textContent = `${round(v * 25.4, 2)} mm`; } function fromCm() { if (convertLock) return; const v = parseFloat(cCm.value); if (!isFinite(v)) { cInch.value = ""; cInchFrac.textContent = " "; cMm.textContent = cCm.value ? "Enter a number" : " "; return; } const inches = v / 2.54; convertLock = true; cInch.value = round(inches, 4).toString(); convertLock = false; cInchFrac.textContent = `≈ ${nearestEighth(inches)} (decimal ${round(inches,3)})`; cMm.textContent = `${round(v * 10, 2)} mm`; } if (cInch && cCm) { cInch.addEventListener("input", fromInch); cCm.addEventListener("input", fromCm); document.querySelectorAll("#c-quick .chip").forEach(b => { b.addEventListener("click", () => { cInch.value = b.dataset.inch; fromInch(); cInch.focus(); }); }); } /* ---------- Bias ---------- */ function calcBias() { const units = $("b-units").value; const length = parseFloat($("b-length").value); const finished = parseFloat($("b-finished").value); const mult = parseFloat($("b-fold").value); const out = $("b-result"); if (![length, finished].every(v => isFinite(v) && v > 0)) { out.className = "result"; out.innerHTML = "

Enter values

Fill in total length and finished width to see the square size.

"; return; } const stripWidth = finished * mult; // Continuous bias from a square: total length ≈ side² / strip_width // → side = sqrt(length × strip_width) const side = Math.sqrt(length * stripWidth); const sideWithBuffer = side * 1.05; // ~5% buffer for joins & trim const numStripsFromRect = Math.ceil(length / Math.max(1e-6, side)); // fallback method out.className = "result"; if (units === "in") { out.innerHTML = `

Cut a square

${round(sideWithBuffer,1)} in × ${round(sideWithBuffer,1)} in
`; } else { out.innerHTML = `

Cut a square

${round(sideWithBuffer,1)} cm × ${round(sideWithBuffer,1)} cm
`; } } ["b-units","b-length","b-finished","b-fold"].forEach(id => { const el = $(id); if (el) el.addEventListener("input", calcBias); }); /* ---------- Buttons ---------- */ function calcButtons() { const units = $("bt-units").value; const length = parseFloat($("bt-length").value); const top = parseFloat($("bt-top").value); const bottom = parseFloat($("bt-bottom").value); const count = Math.max(2, parseInt($("bt-count").value, 10) || 2); const out = $("bt-result"); if (![length, top, bottom].every(v => isFinite(v) && v >= 0)) { out.className = "result"; out.innerHTML = "

Enter values

Fill in placket length and offsets to see button positions.

"; return; } if (top + bottom >= length) { out.className = "result warn"; out.innerHTML = `

Offsets too large

Top + bottom offsets (${round(top+bottom,2)} ${units}) leave no room on a placket of ${length} ${units}.

`; return; } const usable = length - top - bottom; const gap = usable / (count - 1); const positions = []; for (let i = 0; i < count; i++) { positions.push(round(top + gap * i, 2)); } const list = positions.map((p,i) => `
  • Button ${i+1}: ${p} ${units} from top
  • `).join(""); out.className = "result"; out.innerHTML = `

    Spacing

    ${round(gap,2)} ${units} between buttons
    `; } ["bt-units","bt-length","bt-top","bt-bottom","bt-count"].forEach(id => { const el = $(id); if (el) el.addEventListener("input", calcButtons); }); /* ---------- Reference data ---------- */ const NEEDLES = [ { type: "Universal", size: "60/8", uses: "Sheers, fine batiste, organza" }, { type: "Universal", size: "70/10", uses: "Lightweight cotton, voile, fine linen" }, { type: "Universal", size: "80/12", uses: "Quilting cotton, calico, medium linen — most-used size" }, { type: "Universal", size: "90/14", uses: "Heavier cotton, broadcloth, lightweight wool" }, { type: "Universal", size: "100/16", uses: "Denim, canvas, heavy linen" }, { type: "Jeans/Denim", size: "90–100", uses: "Denim, twill, dense weaves; sharp tip pierces cleanly" }, { type: "Microtex/Sharp", size: "70–90", uses: "Silk, microfiber, tightly-woven cotton, foundation paper piecing" }, { type: "Stretch", size: "75/11", uses: "Lycra/spandex, swimsuit, knits prone to skipped stitches" }, { type: "Ballpoint/Jersey", size: "80/12", uses: "Jersey, interlock, knits — slips between fibres" }, { type: "Leather", size: "80–100", uses: "Real leather, suede, vinyl (do NOT use on woven cloth)" }, { type: "Topstitch", size: "90–100", uses: "Heavy or doubled topstitching, decorative thread" }, { type: "Twin", size: "2.0–4.0 mm", uses: "Parallel rows; hems on knits with built-in zigzag underside" }, ]; function renderNeedles() { const root = $("needles-grid"); if (!root) return; root.innerHTML = NEEDLES.map(n => `
    ${n.size}

    ${n.type}

    ${n.uses}

    `).join(""); } const THREADS = [ { wt: "100", tex: "10", denier: "90", name: "Fine silk / heirloom", uses: "Lace, heirloom, very fine silks; nearly invisible" }, { wt: "60", tex: "16", denier: "150", name: "Fine bobbin / appliqué", uses: "Machine appliqué, free-motion quilting, bobbin thread for embroidery" }, { wt: "50", tex: "20", denier: "180", name: "All-purpose", uses: "Garment construction, default seam thread (Gütermann Sew-All, Coats Dual Duty)" }, { wt: "40", tex: "25", denier: "225", name: "Embroidery / piecing", uses: "Machine embroidery, quilt piecing where seam bulk matters" }, { wt: "30", tex: "33", denier: "300", name: "Topstitch", uses: "Visible topstitching, jeans, denim hems" }, { wt: "12", tex: "80", denier: "720", name: "Heavy decorative", uses: "Sashiko, big-stitch quilting, bold topstitching (size 90–100 needle)" }, ]; function renderThread() { const root = $("thread-grid"); if (!root) return; root.innerHTML = THREADS.map((t,i) => `
    ${t.wt} Wt · Tex ${t.tex} · ${t.denier} D

    ${t.name}

    ${t.uses}

    `).join(""); } const STITCHES = [ { name: "Basting", length: "4–5 mm", uses: "Long temporary stitches, easy to remove" }, { name: "Construction (default)", length: "2.5 mm", uses: "Most seams on medium-weight woven fabric" }, { name: "Lightweight fabric", length: "1.5–2 mm", uses: "Voile, batiste, silk — keeps seams from puckering" }, { name: "Heavy fabric", length: "3–3.5 mm", uses: "Denim, canvas, upholstery weight" }, { name: "Topstitch", length: "3–3.5 mm", uses: "Visible decorative or structural top stitching" }, { name: "Edgestitch", length: "2–2.5 mm", uses: "Close to fabric edge for a crisp finish" }, { name: "Stay stitch", length: "2 mm", uses: "Stabilises curves (necklines, armholes) before sewing" }, { name: "Zigzag finish", length: "2.5 mm L × 4 mm W", uses: "Finishing raw edges to prevent fraying" }, { name: "Buttonhole", length: "0.4–0.5 mm L × 1.5–2 mm W", uses: "Tight satin stitch around the opening" }, { name: "Gathering", length: "5 mm", uses: "Two parallel rows; loosen tension and pull bobbin threads" }, ]; function renderStitches() { const root = $("stitches-grid"); if (!root) return; root.innerHTML = STITCHES.map(s => `
    ${s.length}

    ${s.name}

    ${s.uses}

    `).join(""); } const CARE = [ { group: "Washing", items: [ ["wash-normal-30.svg", "Normal wash 30°C", "Machine wash, normal cycle"], ["wash-normal-40.svg", "Normal wash 40°C", "Machine wash, normal cycle"], ["wash-normal-50.svg", "Normal wash 50°C", "Machine wash, normal cycle"], ["wash-normal-60.svg", "Normal wash 60°C", "Machine wash, normal cycle"], ["wash-normal-70.svg", "Normal wash 70°C", "Machine wash, normal cycle"], ["wash-normal-95.svg", "Normal wash 95°C", "Boil wash"], ["wash-gentle-30.svg", "Gentle wash 30°C", "Permanent press cycle"], ["wash-gentle-40.svg", "Gentle wash 40°C", "Permanent press cycle"], ["wash-very-gentle-30.svg", "Very gentle wash 30°C", "Delicate cycle"], ["wash-very-gentle-40.svg", "Very gentle wash 40°C", "Delicate cycle"], ["wash-hand.svg", "Hand wash", "Max 40°C, do not wring"], ["wash-do-not.svg", "Do not wash", "Cannot be cleaned with water"], ]}, { group: "Bleaching", items: [ ["bleach-any.svg", "Any bleach", "Chlorine or non-chlorine"], ["bleach-non-chlorine.svg", "Non-chlorine bleach", "Oxygen-based only"], ["bleach-do-not.svg", "Do not bleach", ""], ]}, { group: "Drying", items: [ ["dry-tumble-normal-low.svg", "Tumble dry low", "Normal cycle, max 60°C"], ["dry-tumble-normal-medium.svg", "Tumble dry medium", "Normal cycle, max 80°C"], ["dry-tumble-normal-high.svg", "Tumble dry high", "Normal cycle, max 95°C"], ["dry-tumble-gentle-low.svg", "Tumble dry gentle", "Permanent press, low heat"], ["dry-tumble-do-not.svg", "Do not tumble dry", ""], ["dry-line.svg", "Line dry", "Hang on a line or hanger"], ["dry-flat.svg", "Dry flat", "Lay flat to dry"], ["dry-drip.svg", "Drip dry", "Hang soaking wet, no wringing"], ]}, { group: "Ironing", items: [ ["iron-low.svg", "Iron low", "Max 110°C — synthetics, acrylic"], ["iron-medium.svg", "Iron medium", "Max 150°C — wool, silk, polyester"], ["iron-high.svg", "Iron high", "Max 200°C — cotton, linen"], ["iron-do-not.svg", "Do not iron", ""], ]}, { group: "Dry cleaning", items: [ ["dryclean-any.svg", "Dry clean", "Any solvent"], ["dryclean-petroleum.svg", "Petroleum solvent", "Hydrocarbon solvents only"], ["dryclean-fluorocarbon.svg", "Fluorocarbon solvent", "Fluorocarbon or petroleum"], ["dryclean-do-not.svg", "Do not dry clean", ""], ]}, ]; function renderCare() { const root = $("care-groups"); if (!root) return; root.innerHTML = CARE.map(g => `

    ${g.group}

    ${g.items.map(([svg,name,desc]) => `
    ${name}${desc ? ` — ${desc}` : ''}
    `).join("")}
    `).join(""); } /* ---------- Init ---------- */ /* ---------- Affiliate links (Amazon UK) ---------- */ function initAffiliates() { if (!AMAZON_TAG_UK) return; const tag = encodeURIComponent(AMAZON_TAG_UK); document.querySelectorAll("[data-buy-search]").forEach(a => { const q = encodeURIComponent(a.dataset.buySearch); a.href = "https://www.amazon.co.uk/s?k=" + q + "&tag=" + tag; a.target = "_blank"; a.rel = "noopener sponsored"; }); document.querySelectorAll(".buy-row").forEach(el => el.hidden = false); const disc = document.getElementById("affiliate-disclosure"); if (disc) disc.hidden = false; } /* ---------- Support link (Ko-fi) ---------- */ function initSupport() { if (!KOFI_USERNAME) return; const a = document.getElementById("support-link"); if (!a) return; a.href = "https://ko-fi.com/" + encodeURIComponent(KOFI_USERNAME); a.target = "_blank"; a.rel = "noopener"; a.hidden = false; } function initConsent() { const banner = document.getElementById("consent-banner"); if (!banner) return; let stored = null; try { stored = localStorage.getItem("asksewphie-consent"); } catch (e) {} if (!stored) banner.hidden = false; function setChoice(choice) { try { localStorage.setItem("asksewphie-consent", choice); } catch (e) {} if (typeof window.gtag === "function") { window.gtag("consent", "update", { "analytics_storage": choice === "granted" ? "granted" : "denied" }); } banner.hidden = true; } const accept = document.getElementById("consent-accept"); const reject = document.getElementById("consent-reject"); const manage = document.getElementById("manage-cookies"); if (accept) accept.addEventListener("click", () => setChoice("granted")); if (reject) reject.addEventListener("click", () => setChoice("denied")); if (manage) manage.addEventListener("click", (e) => { e.preventDefault(); banner.hidden = false; }); } function init() { renderNeedles(); renderThread(); renderStitches(); renderCare(); calcYardage(); calcBias(); calcButtons(); initAffiliates(); initSupport(); initConsent(); // Migrate any old hash-style URL (e.g. someone landed on /#yardage from a bookmark) const legacy = (location.hash || "").replace(/^#/, "").trim(); if (legacy && ROUTES.includes(legacy) && location.pathname === "/") { const m = ROUTE_META[legacy]; history.replaceState(null, "", m.path); } show(routeFromPath()); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();