/* 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}
- ${piecesPerRow} piece${piecesPerRow!==1?"s":""} fit across the fabric width
- ${rows} row${rows!==1?"s":""} of pieces × ${round(len,2)} ${units} = ${round(baseLen,1)} ${units} before allowances
- +${Math.round(shrink*100)}% shrinkage, +${Math.round(seam*100)}% seam/cutting allowance
`;
}
["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
- Strip width to cut: ${round(stripWidth,2)} in (${mult}× finished width)
- Theoretical minimum square: ${round(side,1)} in (we add 5% for joins & trim)
- Alternative: cut ${numStripsFromRect} bias strips ${round(stripWidth,2)} in wide from a rectangle
`;
} else {
out.innerHTML = `
Cut a square
${round(sideWithBuffer,1)} cm × ${round(sideWithBuffer,1)} cm
- Strip width to cut: ${round(stripWidth,2)} cm (${mult}× finished width)
- Theoretical minimum square: ${round(side,1)} cm (we add 5% for joins & trim)
- Alternative: cut ${numStripsFromRect} bias strips ${round(stripWidth,2)} cm wide from a rectangle
`;
}
}
["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();
}
})();