Yarn
Knit for the Girls & Guys Exclusive - Pink Ladies (Fade 2) - Stirling 4ply
SKU THDST4083
Pink Ladies is colourway 2 in Skein Machine's exclusive Knit for the Girls & Guys (KFTG) charity fade-pack — a limited Australian hand-dyed yarn where 20% of every sale goes directly to cancer research (10% to the Breast Cancer Foundation, 10% to the Prostate Cancer Foundation of Australia).
<?php// Generate the corrected HTML with centered SVG canvas and dynamic polygons$html = '<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive SVG Workspace</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; } body { background: #ffffff; color: #333; min-height: 100vh; overflow: hidden; } .workspace-container { display: flex; height: 100vh; width: 100vw; } .workspace-panel { flex: 1; position: relative; background: #ffffff; overflow: hidden; } .workspace-header { position: absolute; top: 15px; left: 15px; z-index: 10; background: rgba(255, 255, 255, 0.8); border-radius: 50px; padding: 8px 15px; display: flex; align-items: center; gap: 10px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } .reset-btn { background: #ff6b6b; border: none; border-radius: 50%; width: 40px; height: 40px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } .reset-btn:hover { background: #ff5252; transform: scale(1.1); } .reset-btn svg { width: 20px; height: 20px; } .zoom-info { font-size: 0.9rem; padding: 0 10px; color: #333; } #workspace { width: 100%; height: 100%; cursor: grab; background-color: #ffffff; } #workspace:active { cursor: grabbing; } .config-panel { width: 300px; background: #f8f9fa; border-left: 1px solid #e0e0e0; display: flex; flex-direction: column; transition: all 0.3s ease; box-shadow: -2px 0 5px rgba(0, 0, 0, 0.05); } .panel-header { display: flex; background: #ffffff; border-bottom: 1px solid #e0e0e0; } .tab { flex: 1; padding: 15px; text-align: center; cursor: pointer; font-weight: 600; transition: all 0.3s ease; border-bottom: 3px solid transparent; color: #666; } .tab.active { background: #e3f2fd; border-bottom: 3px solid #2196f3; color: #2196f3; } .tab:hover:not(.active) { background: #f5f5f5; } .panel-content { padding: 25px; overflow-y: auto; flex: 1; } .setting-group { margin-bottom: 25px; } .setting-label { display: flex; justify-content: space-between; margin-bottom: 10px; font-weight: 500; color: #555; } .slider-container { background: #ffffff; padding: 15px; border-radius: 10px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); } .slider { width: 100%; height: 10px; -webkit-appearance: none; background: linear-gradient(to right, #4285f4, #34a853, #fbbc05, #ea4335); border-radius: 5px; outline: none; } .slider::-webkit-slider-thumb { -webkit-appearance: none; width: 22px; height: 22px; border-radius: 50%; background: white; cursor: pointer; box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); } .value-display { text-align: center; font-size: 1.8rem; font-weight: 700; margin: 15px 0; color: #2196f3; } .toggle-panel { position: absolute; top: 50%; right: -30px; transform: translateY(-50%); background: #f8f9fa; border: 1px solid #e0e0e0; border-left: none; border-radius: 0 5px 5px 0; width: 30px; height: 60px; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 3px 0 5px rgba(0, 0, 0, 0.1); z-index: 100; } .toggle-panel svg { width: 15px; height: 15px; transition: transform 0.3s ease; } .collapsed .config-panel { width: 0; opacity: 0; overflow: hidden; } .collapsed .toggle-panel svg { transform: rotate(180deg); } .collapsed .workspace-panel { flex: 1; } @media (max-width: 768px) { .config-panel { width: 250px; } } </style></head><body> <div class="workspace-container"> <div class="workspace-panel"> <div class="workspace-header"> <button class="reset-btn" id="resetBtn" title="Reset View"> <svg viewBox="0 0 24 24" fill="white"> <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/> </svg> </button> <div class="zoom-info">Zoom: <span id="zoomValue">100%</span></div> </div> <svg id="workspace" xmlns="http://www.w3.org/2000/svg" viewBox="-500 -500 1000 1000"> <!-- Polygon elements will be added dynamically --> </svg> </div> <div class="config-panel"> <div class="panel-header"> <div class="tab active" data-tab="S">S</div> <div class="tab" data-tab="M">M</div> <div class="tab" data-tab="L">L</div> </div> <div class="panel-content"> <div class="setting-group"> <div class="setting-label"> <span>Rows</span> <span id="rowsValue">5</span> </div> <div class="slider-container"> <input type="range" min="1" max="250" value="5" class="slider" id="rowsSlider"> </div> <div class="value-display" id="rowsDisplay">5</div> </div> <div class="setting-group"> <div class="setting-label"> <span>Cols (Points)</span> <span id="colsValue">12</span> </div> <div class="slider-container"> <input type="range" min="12" max="200" value="12" class="slider" id="colsSlider"> </div> <div class="value-display" id="colsDisplay">12</div> </div> <div class="setting-group"> <div class="setting-label"> <span>Increase Rows</span> <span id="increaseRowsValue">1</span> </div> <div class="slider-container"> <input type="range" min="1" max="20" value="1" class="slider" id="increaseRowsSlider"> </div> <div class="value-display" id="increaseRowsDisplay">1</div> </div> <div class="setting-group"> <div class="setting-label"> <span>Increase Amount</span> <span id="increaseAmountValue">2</span> </div> <div class="slider-container"> <input type="range" min="1" max="10" value="2" class="slider" id="increaseAmountSlider"> </div> <div class="value-display" id="increaseAmountDisplay">2</div> </div> <div class="setting-group"> <div class="setting-label"> <span>Border Width</span> <span id="borderValue">2px</span> </div> <div class="slider-container"> <input type="range" min="1" max="10" value="2" class="slider" id="borderSlider"> </div> <div class="value-display" id="borderDisplay">2px</div> </div> </div> </div> <button class="toggle-panel" id="togglePanel"> <svg viewBox="0 0 24 24" fill="#666"> <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/> </svg> </button> </div>
<script> // DOM Elements const workspace = document.getElementById("workspace"); const rowsSlider = document.getElementById("rowsSlider"); const rowsValue = document.getElementById("rowsValue"); const rowsDisplay = document.getElementById("rowsDisplay"); const colsSlider = document.getElementById("colsSlider"); const colsValue = document.getElementById("colsValue"); const colsDisplay = document.getElementById("colsDisplay"); const increaseRowsSlider = document.getElementById("increaseRowsSlider"); const increaseRowsValue = document.getElementById("increaseRowsValue"); const increaseRowsDisplay = document.getElementById("increaseRowsDisplay"); const increaseAmountSlider = document.getElementById("increaseAmountSlider"); const increaseAmountValue = document.getElementById("increaseAmountValue"); const increaseAmountDisplay = document.getElementById("increaseAmountDisplay"); const borderSlider = document.getElementById("borderSlider"); const borderValue = document.getElementById("borderValue"); const borderDisplay = document.getElementById("borderDisplay"); const resetBtn = document.getElementById("resetBtn"); const zoomValue = document.getElementById("zoomValue"); const tabs = document.querySelectorAll(".tab"); const togglePanel = document.getElementById("togglePanel"); const container = document.querySelector(".workspace-container"); // State variables let currentTab = "S"; let presets = { S: { rows: 5, cols: 12, increaseRows: 1, increaseAmount: 2, borderWidth: 2 }, M: { rows: 5, cols: 24, increaseRows: 2, increaseAmount: 3, borderWidth: 2 }, L: { rows: 5, cols: 48, increaseRows: 3, increaseAmount: 4, borderWidth: 2 } }; // ViewBox state for SVG zoom/pan (centered at origin) let viewState = { x: -500, y: -500, w: 1000, h: 1000, isPanning: false, startPoint: { x: 0, y: 0 } }; // Store original viewbox settings for reset const originalViewBox = { x: -500, y: -500, w: 1000, h: 1000 }; // Polygon points tracking let polygonPoints = {}; // Store points for each polygon by ID let polygonPointData = {}; // Store detailed point data with decimal naming // Initialize the workspace function initWorkspace() { // Set initial view resetView(); // Create the polygon elements createPolygons(); // Set up event listeners setupEventListeners(); // Apply initial preset applyPreset(currentTab); } // Create the polygon elements function createPolygons() { // Clear existing polygons and lines const existingElements = document.querySelectorAll(".dynamicPolygon, .connectionLine, .increaseLine"); existingElements.forEach(element => element.remove()); // Create new polygons updatePolygons(); } // Generate points for a polygon with specified number of sides function generatePolygonPoints(sides, radius, centerX = 0, centerY = 0) { const points = []; for (let i = 0; i < sides; i++) { const angle = (i * 2 * Math.PI) / sides - Math.PI / 2; // Start from top const x = centerX + radius * Math.cos(angle); const y = centerY + radius * Math.sin(angle); points.push([x, y]); } return points; } // Update polygons based on current settings function updatePolygons() { // Clear existing polygons and lines const existingElements = document.querySelectorAll(".dynamicPolygon, .connectionLine, .increaseLine"); existingElements.forEach(element => element.remove()); const rows = presets[currentTab].rows; const cols = presets[currentTab].cols; const increaseRows = presets[currentTab].increaseRows; const increaseAmount = presets[currentTab].increaseAmount; const borderWidth = presets[currentTab].borderWidth; // Base radius (half of workspace width) const baseRadius = 500 * 0.5; // Half of 1000 (workspace width) // Clear previous points tracking polygonPoints = {}; polygonPointData = {}; // Create base polygons (scaled up from original) for (let i = 0; i < rows; i++) { const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon"); polygon.classList.add("dynamicPolygon"); polygon.setAttribute("fill", "transparent"); polygon.setAttribute("stroke", "black"); polygon.setAttribute("stroke-width", borderWidth); // Calculate radius for this polygon (scaled up) const scaleFactor = 1 + (i * 0.2); // Scale up by 20% per row const radius = baseRadius * scaleFactor; // Generate base points for this polygon let points = generatePolygonPoints(cols, radius, 0, 0); // Initialize point data with original indices polygonPointData[`polygon-${i}`] = {}; for (let j = 0; j < points.length; j++) { polygonPointData[`polygon-${i}`][j] = { x: points[j][0], y: points[j][1], isIncrement: false }; } // Store points for this polygon polygonPoints[`polygon-${i}`] = points; // Convert points to string format const pointsString = points.map(point => point.join(", ")).join(" "); polygon.setAttribute("points", pointsString); workspace.appendChild(polygon); } // Create increase lines for specified rows if (increaseRows > 0 && rows > 1 && increaseAmount > 0) { createIncreaseLines(); } // Create connection lines if we have at least 2 polygons if (rows >= 2) { // Get points from first and last polygons const firstPolygonPoints = polygonPoints["polygon-0"]; const lastPolygonPoints = polygonPoints[`polygon-${rows-1}`]; // Create lines for each point const maxPoints = Math.max(firstPolygonPoints.length, lastPolygonPoints.length); for (let i = 0; i < maxPoints; i++) { const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); line.classList.add("connectionLine"); line.setAttribute("stroke", "black"); line.setAttribute("stroke-width", borderWidth * 0.5); // Thinner lines // Get points (wrap around if needed) const firstPointIndex = i % firstPolygonPoints.length; const lastPointIndex = i % lastPolygonPoints.length; const firstPoint = firstPolygonPoints[firstPointIndex]; const lastPoint = lastPolygonPoints[lastPointIndex]; line.setAttribute("x1", firstPoint[0]); line.setAttribute("y1", firstPoint[1]); line.setAttribute("x2", lastPoint[0]); line.setAttribute("y2", lastPoint[1]); workspace.appendChild(line); } } } // Create increase lines based on new logic with decimal naming function createIncreaseLines() { const rows = presets[currentTab].rows; const increaseRows = presets[currentTab].increaseRows; const increaseAmount = presets[currentTab].increaseAmount; const borderWidth = presets[currentTab].borderWidth; // For each row that should be increased (every increaseRows-th row starting from row 1) for (let i = 1; i < rows; i++) { if (increaseRows > 1 && (i + 1) % increaseRows !== 0) continue; const polygonPointsCurrent = polygonPoints[`polygon-${i}`]; const pointData = polygonPointData[`polygon-${i}`]; if (!polygonPointsCurrent) continue; // Determine which points to use based on increaseAmount const step = Math.max(1, Math.floor(polygonPointsCurrent.length / increaseAmount)); // Keep track of new points to add after processing const newPointsToAdd = []; // Create lines for selected points for (let j = 0; j < increaseAmount; j++) { const pointIndex = j * step; if (pointIndex >= polygonPointsCurrent.length) break; // Get current point and next point const currentPoint = polygonPointsCurrent[pointIndex]; const nextPointIndex = (pointIndex + 1) % polygonPointsCurrent.length; const nextPoint = polygonPointsCurrent[nextPointIndex]; // Calculate midpoint between current and next point const midX = (currentPoint[0] + nextPoint[0]) / 2; const midY = (currentPoint[1] + nextPoint[1]) / 2; // Find corresponding point on outermost polygon const outerPolygonPoints = polygonPoints[`polygon-${rows-1}`]; let closestOuterPoint = outerPolygonPoints[0]; let minDistance = Number.MAX_VALUE; // Find the closest point on the outer polygon for (let k = 0; k < outerPolygonPoints.length; k++) { const outerPoint = outerPolygonPoints[k]; const distance = Math.sqrt( Math.pow(outerPoint[0] - midX, 2) + Math.pow(outerPoint[1] - midY, 2) ); if (distance < minDistance) { minDistance = distance; closestOuterPoint = outerPoint; } } // Create line from midpoint to closest outer point const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); line.classList.add("increaseLine"); line.setAttribute("stroke", "red"); line.setAttribute("stroke-width", borderWidth); line.setAttribute("x1", midX); line.setAttribute("y1", midY); line.setAttribute("x2", closestOuterPoint[0]); line.setAttribute("y2", closestOuterPoint[1]); workspace.appendChild(line); // Add the midpoint as a new point with decimal naming const decimalName = pointIndex + 0.5; newPointsToAdd.push({ index: pointIndex + 1, point: [midX, midY], decimalName: decimalName }); // Store point data with decimal naming polygonPointData[`polygon-${i}`][decimalName] = { x: midX, y: midY, isIncrement: true }; } // Add new points to the polygon in order to maintain sequence let insertedCount = 0; for (const newPoint of newPointsToAdd) { polygonPoints[`polygon-${i}`].splice(newPoint.index + insertedCount, 0, newPoint.point); insertedCount++; } // Update the polygon with new points const polygonElements = document.querySelectorAll(".dynamicPolygon"); if (polygonElements[i]) { const pointsString = polygonPoints[`polygon-${i}`].map(point => point.join(", ")).join(" "); polygonElements[i].setAttribute("points", pointsString); } } } // Reset view to default (centered at origin) function resetView() { viewState = { x: originalViewBox.x, y: originalViewBox.y, w: originalViewBox.w, h: originalViewBox.h, isPanning: false, startPoint: { x: 0, y: 0 } }; workspace.setAttribute("viewBox", `${viewState.x} ${viewState.y} ${viewState.w} ${viewState.h}`); zoomValue.textContent = "100%"; } // Apply preset settings function applyPreset(tab) { const preset = presets[tab]; rowsSlider.value = preset.rows; rowsValue.textContent = `${preset.rows}`; rowsDisplay.textContent = `${preset.rows}`; colsSlider.value = preset.cols; colsValue.textContent = `${preset.cols}`; colsDisplay.textContent = `${preset.cols}`; increaseRowsSlider.value = preset.increaseRows; increaseRowsValue.textContent = `${preset.increaseRows}`; increaseRowsDisplay.textContent = `${preset.increaseRows}`; increaseAmountSlider.value = preset.increaseAmount; increaseAmountValue.textContent = `${preset.increaseAmount}`; increaseAmountDisplay.textContent = `${preset.increaseAmount}`; borderSlider.value = preset.borderWidth; borderValue.textContent = `${preset.borderWidth}px`; borderDisplay.textContent = `${preset.borderWidth}px`; updatePolygons(); } // Helper function to convert screen coordinates to SVG coordinates function getSVGPoint(e) { const p = workspace.createSVGPoint(); p.x = e.clientX; p.y = e.clientY; return p.matrixTransform(workspace.getScreenCTM().inverse()); } // Set up event listeners function setupEventListeners() { // Tab switching tabs.forEach(tab => { tab.addEventListener("click", () => { // Update active tab tabs.forEach(t => t.classList.remove("active")); tab.classList.add("active"); // Apply preset currentTab = tab.dataset.tab; applyPreset(currentTab); }); }); // Rows slider input rowsSlider.addEventListener("input", () => { const value = parseInt(rowsSlider.value); presets[currentTab].rows = value; rowsValue.textContent = `${value}`; rowsDisplay.textContent = `${value}`; updatePolygons(); }); // Cols slider input colsSlider.addEventListener("input", () => { const value = parseInt(colsSlider.value); presets[currentTab].cols = value; colsValue.textContent = `${value}`; colsDisplay.textContent = `${value}`; updatePolygons(); }); // Increase rows slider input increaseRowsSlider.addEventListener("input", () => { const value = parseInt(increaseRowsSlider.value); presets[currentTab].increaseRows = value; increaseRowsValue.textContent = `${value}`; increaseRowsDisplay.textContent = `${value}`; updatePolygons(); }); // Increase amount slider input increaseAmountSlider.addEventListener("input", () => { const value = parseInt(increaseAmountSlider.value); presets[currentTab].increaseAmount = value; increaseAmountValue.textContent = `${value}`; increaseAmountDisplay.textContent = `${value}`; updatePolygons(); }); // Border width slider input borderSlider.addEventListener("input", () => { const value = parseInt(borderSlider.value); presets[currentTab].borderWidth = value; borderValue.textContent = `${value}px`; borderDisplay.textContent = `${value}px`; updatePolygons(); }); // Reset button resetBtn.addEventListener("click", resetView); // Toggle panel togglePanel.addEventListener("click", () => { container.classList.toggle("collapsed"); }); // Zoom with mouse wheel workspace.addEventListener("wheel", (e) => { e.preventDefault(); // Determine zoom factor const zoomFactor = e.deltaY < 0 ? 0.9 : 1.1; // Don\'t let it get too small or large if (viewState.w * zoomFactor > 5000 || viewState.w * zoomFactor < 100) return; // Get the mouse position relative to the SVG internal space const mouseSVGPoint = getSVGPoint(e); // Calculate new width and height const newW = viewState.w * zoomFactor; const newH = viewState.h * zoomFactor; // Adjust X and Y so the mouse pointer remains the anchor center of the zoom const newX = mouseSVGPoint.x - (mouseSVGPoint.x - viewState.x) * zoomFactor; const newY = mouseSVGPoint.y - (mouseSVGPoint.y - viewState.y) * zoomFactor; // Update state and DOM viewState.x = newX; viewState.y = newY; viewState.w = newW; viewState.h = newH; workspace.setAttribute("viewBox", `${viewState.x} ${viewState.y} ${viewState.w} ${viewState.h}`); zoomValue.textContent = `${Math.round(1000 / viewState.w * 100)}%`; }); // Pan with middle mouse button workspace.addEventListener("pointerdown", (e) => { if (e.button === 1) { // Middle mouse button viewState.isPanning = true; // Store the exact SVG coordinate where the user clicked viewState.startPoint = getSVGPoint(e); workspace.style.cursor = "grabbing"; e.preventDefault(); } }); workspace.addEventListener("pointermove", (e) => { if (!viewState.isPanning) return; // Get the current mouse position in SVG coordinates const currentPoint = getSVGPoint(e); // Calculate how much the mouse moved inside the SVG space const dx = viewState.startPoint.x - currentPoint.x; const dy = viewState.startPoint.y - currentPoint.y; // Shift the viewBox by the delta movement viewState.x += dx; viewState.y += dy; workspace.setAttribute("viewBox", `${viewState.x} ${viewState.y} ${viewState.w} ${viewState.h}`); }); workspace.addEventListener("pointerup", () => { if (viewState.isPanning) { viewState.isPanning = false; workspace.style.cursor = "grab"; } }); workspace.addEventListener("pointerleave", () => { if (viewState.isPanning) { viewState.isPanning = false; workspace.style.cursor = "grab"; } }); // Handle window resize window.addEventListener("resize", () => { updatePolygons(); }); } // Initialize when page loads window.addEventListener("DOMContentLoaded", initWorkspace); </script></body></html>';
// Output the HTMLecho $html;?>
Imagine knitting with the last warm light of a summer evening — deep, moody hues that shift and glow as your project grows. Each skein is hand-dyed in small batches in South Australia.
This colourway is part of a 5-colour fade-pack — buy the full set or individual skeins. See all KFTG&G colourways and bases →
Yarn Specifications
Weight: 4ply / Fingering
Fibre: Singlespun 60% Superwash Merino, 20% Baby Alpaca, 5% Grade A Mullberry Silk
Yardage: 460m per 115g skein
Dyed in: South Australia, Australia
Frequently Asked Questions
What is KFTG? Knit for the Girls is an annual fibre arts community fundraiser raising money for breast cancer research. Skein Machine has partnered with Louie and Lola Yarns to extend donations to the Prostate Cancer Foundation of Australia — so we Knit for the Girls and Guys.
The Yarn? Yes — 60% Superwash Merino makes it machine-washable and soft against the skin, blended with 20% Alpaca and 20% Grade A Mulberry Silk to give a great drape to your project.
Can I buy just one skein from the fade? Absolutely — individual skeins are available. The full 5-colour fade-pack is also available on the KFTG Skein Machine page. This listing is for one skein.
Where is this yarn made? Hand-dyed in South Australia by Skein Machine.
<?php// Generate the corrected HTML with centered SVG canvas and dynamic polygons$html = '<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive SVG Workspace</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; } body { background: #ffffff; color: #333; min-height: 100vh; overflow: hidden; } .workspace-container { display: flex; height: 100vh; width: 100vw; } .workspace-panel { flex: 1; position: relative; background: #ffffff; overflow: hidden; } .workspace-header { position: absolute; top: 15px; left: 15px; z-index: 10; background: rgba(255, 255, 255, 0.8); border-radius: 50px; padding: 8px 15px; display: flex; align-items: center; gap: 10px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } .reset-btn { background: #ff6b6b; border: none; border-radius: 50%; width: 40px; height: 40px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } .reset-btn:hover { background: #ff5252; transform: scale(1.1); } .reset-btn svg { width: 20px; height: 20px; } .zoom-info { font-size: 0.9rem; padding: 0 10px; color: #333; } #workspace { width: 100%; height: 100%; cursor: grab; background-color: #ffffff; } #workspace:active { cursor: grabbing; } .config-panel { width: 300px; background: #f8f9fa; border-left: 1px solid #e0e0e0; display: flex; flex-direction: column; transition: all 0.3s ease; box-shadow: -2px 0 5px rgba(0, 0, 0, 0.05); } .panel-header { display: flex; background: #ffffff; border-bottom: 1px solid #e0e0e0; } .tab { flex: 1; padding: 15px; text-align: center; cursor: pointer; font-weight: 600; transition: all 0.3s ease; border-bottom: 3px solid transparent; color: #666; } .tab.active { background: #e3f2fd; border-bottom: 3px solid #2196f3; color: #2196f3; } .tab:hover:not(.active) { background: #f5f5f5; } .panel-content { padding: 25px; overflow-y: auto; flex: 1; } .setting-group { margin-bottom: 25px; } .setting-label { display: flex; justify-content: space-between; margin-bottom: 10px; font-weight: 500; color: #555; } .slider-container { background: #ffffff; padding: 15px; border-radius: 10px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); } .slider { width: 100%; height: 10px; -webkit-appearance: none; background: linear-gradient(to right, #4285f4, #34a853, #fbbc05, #ea4335); border-radius: 5px; outline: none; } .slider::-webkit-slider-thumb { -webkit-appearance: none; width: 22px; height: 22px; border-radius: 50%; background: white; cursor: pointer; box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); } .value-display { text-align: center; font-size: 1.8rem; font-weight: 700; margin: 15px 0; color: #2196f3; } .toggle-panel { position: absolute; top: 50%; right: -30px; transform: translateY(-50%); background: #f8f9fa; border: 1px solid #e0e0e0; border-left: none; border-radius: 0 5px 5px 0; width: 30px; height: 60px; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 3px 0 5px rgba(0, 0, 0, 0.1); z-index: 100; } .toggle-panel svg { width: 15px; height: 15px; transition: transform 0.3s ease; } .collapsed .config-panel { width: 0; opacity: 0; overflow: hidden; } .collapsed .toggle-panel svg { transform: rotate(180deg); } .collapsed .workspace-panel { flex: 1; } @media (max-width: 768px) { .config-panel { width: 250px; } } </style></head><body> <div class="workspace-container"> <div class="workspace-panel"> <div class="workspace-header"> <button class="reset-btn" id="resetBtn" title="Reset View"> <svg viewBox="0 0 24 24" fill="white"> <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/> </svg> </button> <div class="zoom-info">Zoom: <span id="zoomValue">100%</span></div> </div> <svg id="workspace" xmlns="http://www.w3.org/2000/svg" viewBox="-500 -500 1000 1000"> <!-- Polygon elements will be added dynamically --> </svg> </div> <div class="config-panel"> <div class="panel-header"> <div class="tab active" data-tab="S">S</div> <div class="tab" data-tab="M">M</div> <div class="tab" data-tab="L">L</div> </div> <div class="panel-content"> <div class="setting-group"> <div class="setting-label"> <span>Rows</span> <span id="rowsValue">5</span> </div> <div class="slider-container"> <input type="range" min="1" max="250" value="5" class="slider" id="rowsSlider"> </div> <div class="value-display" id="rowsDisplay">5</div> </div> <div class="setting-group"> <div class="setting-label"> <span>Cols (Points)</span> <span id="colsValue">12</span> </div> <div class="slider-container"> <input type="range" min="12" max="200" value="12" class="slider" id="colsSlider"> </div> <div class="value-display" id="colsDisplay">12</div> </div> <div class="setting-group"> <div class="setting-label"> <span>Increase Rows</span> <span id="increaseRowsValue">1</span> </div> <div class="slider-container"> <input type="range" min="1" max="20" value="1" class="slider" id="increaseRowsSlider"> </div> <div class="value-display" id="increaseRowsDisplay">1</div> </div> <div class="setting-group"> <div class="setting-label"> <span>Increase Amount</span> <span id="increaseAmountValue">2</span> </div> <div class="slider-container"> <input type="range" min="1" max="10" value="2" class="slider" id="increaseAmountSlider"> </div> <div class="value-display" id="increaseAmountDisplay">2</div> </div> <div class="setting-group"> <div class="setting-label"> <span>Border Width</span> <span id="borderValue">2px</span> </div> <div class="slider-container"> <input type="range" min="1" max="10" value="2" class="slider" id="borderSlider"> </div> <div class="value-display" id="borderDisplay">2px</div> </div> </div> </div> <button class="toggle-panel" id="togglePanel"> <svg viewBox="0 0 24 24" fill="#666"> <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/> </svg> </button> </div>
<script> // DOM Elements const workspace = document.getElementById("workspace"); const rowsSlider = document.getElementById("rowsSlider"); const rowsValue = document.getElementById("rowsValue"); const rowsDisplay = document.getElementById("rowsDisplay"); const colsSlider = document.getElementById("colsSlider"); const colsValue = document.getElementById("colsValue"); const colsDisplay = document.getElementById("colsDisplay"); const increaseRowsSlider = document.getElementById("increaseRowsSlider"); const increaseRowsValue = document.getElementById("increaseRowsValue"); const increaseRowsDisplay = document.getElementById("increaseRowsDisplay"); const increaseAmountSlider = document.getElementById("increaseAmountSlider"); const increaseAmountValue = document.getElementById("increaseAmountValue"); const increaseAmountDisplay = document.getElementById("increaseAmountDisplay"); const borderSlider = document.getElementById("borderSlider"); const borderValue = document.getElementById("borderValue"); const borderDisplay = document.getElementById("borderDisplay"); const resetBtn = document.getElementById("resetBtn"); const zoomValue = document.getElementById("zoomValue"); const tabs = document.querySelectorAll(".tab"); const togglePanel = document.getElementById("togglePanel"); const container = document.querySelector(".workspace-container"); // State variables let currentTab = "S"; let presets = { S: { rows: 5, cols: 12, increaseRows: 1, increaseAmount: 2, borderWidth: 2 }, M: { rows: 5, cols: 24, increaseRows: 2, increaseAmount: 3, borderWidth: 2 }, L: { rows: 5, cols: 48, increaseRows: 3, increaseAmount: 4, borderWidth: 2 } }; // ViewBox state for SVG zoom/pan (centered at origin) let viewState = { x: -500, y: -500, w: 1000, h: 1000, isPanning: false, startPoint: { x: 0, y: 0 } }; // Store original viewbox settings for reset const originalViewBox = { x: -500, y: -500, w: 1000, h: 1000 }; // Polygon points tracking let polygonPoints = {}; // Store points for each polygon by ID let polygonPointData = {}; // Store detailed point data with decimal naming // Initialize the workspace function initWorkspace() { // Set initial view resetView(); // Create the polygon elements createPolygons(); // Set up event listeners setupEventListeners(); // Apply initial preset applyPreset(currentTab); } // Create the polygon elements function createPolygons() { // Clear existing polygons and lines const existingElements = document.querySelectorAll(".dynamicPolygon, .connectionLine, .increaseLine"); existingElements.forEach(element => element.remove()); // Create new polygons updatePolygons(); } // Generate points for a polygon with specified number of sides function generatePolygonPoints(sides, radius, centerX = 0, centerY = 0) { const points = []; for (let i = 0; i < sides; i++) { const angle = (i * 2 * Math.PI) / sides - Math.PI / 2; // Start from top const x = centerX + radius * Math.cos(angle); const y = centerY + radius * Math.sin(angle); points.push([x, y]); } return points; } // Update polygons based on current settings function updatePolygons() { // Clear existing polygons and lines const existingElements = document.querySelectorAll(".dynamicPolygon, .connectionLine, .increaseLine"); existingElements.forEach(element => element.remove()); const rows = presets[currentTab].rows; const cols = presets[currentTab].cols; const increaseRows = presets[currentTab].increaseRows; const increaseAmount = presets[currentTab].increaseAmount; const borderWidth = presets[currentTab].borderWidth; // Base radius (half of workspace width) const baseRadius = 500 * 0.5; // Half of 1000 (workspace width) // Clear previous points tracking polygonPoints = {}; polygonPointData = {}; // Create base polygons (scaled up from original) for (let i = 0; i < rows; i++) { const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon"); polygon.classList.add("dynamicPolygon"); polygon.setAttribute("fill", "transparent"); polygon.setAttribute("stroke", "black"); polygon.setAttribute("stroke-width", borderWidth); // Calculate radius for this polygon (scaled up) const scaleFactor = 1 + (i * 0.2); // Scale up by 20% per row const radius = baseRadius * scaleFactor; // Generate base points for this polygon let points = generatePolygonPoints(cols, radius, 0, 0); // Initialize point data with original indices polygonPointData[`polygon-${i}`] = {}; for (let j = 0; j < points.length; j++) { polygonPointData[`polygon-${i}`][j] = { x: points[j][0], y: points[j][1], isIncrement: false }; } // Store points for this polygon polygonPoints[`polygon-${i}`] = points; // Convert points to string format const pointsString = points.map(point => point.join(", ")).join(" "); polygon.setAttribute("points", pointsString); workspace.appendChild(polygon); } // Create increase lines for specified rows if (increaseRows > 0 && rows > 1 && increaseAmount > 0) { createIncreaseLines(); } // Create connection lines if we have at least 2 polygons if (rows >= 2) { // Get points from first and last polygons const firstPolygonPoints = polygonPoints["polygon-0"]; const lastPolygonPoints = polygonPoints[`polygon-${rows-1}`]; // Create lines for each point const maxPoints = Math.max(firstPolygonPoints.length, lastPolygonPoints.length); for (let i = 0; i < maxPoints; i++) { const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); line.classList.add("connectionLine"); line.setAttribute("stroke", "black"); line.setAttribute("stroke-width", borderWidth * 0.5); // Thinner lines // Get points (wrap around if needed) const firstPointIndex = i % firstPolygonPoints.length; const lastPointIndex = i % lastPolygonPoints.length; const firstPoint = firstPolygonPoints[firstPointIndex]; const lastPoint = lastPolygonPoints[lastPointIndex]; line.setAttribute("x1", firstPoint[0]); line.setAttribute("y1", firstPoint[1]); line.setAttribute("x2", lastPoint[0]); line.setAttribute("y2", lastPoint[1]); workspace.appendChild(line); } } } // Create increase lines based on new logic with decimal naming function createIncreaseLines() { const rows = presets[currentTab].rows; const increaseRows = presets[currentTab].increaseRows; const increaseAmount = presets[currentTab].increaseAmount; const borderWidth = presets[currentTab].borderWidth; // For each row that should be increased (every increaseRows-th row starting from row 1) for (let i = 1; i < rows; i++) { if (increaseRows > 1 && (i + 1) % increaseRows !== 0) continue; const polygonPointsCurrent = polygonPoints[`polygon-${i}`]; const pointData = polygonPointData[`polygon-${i}`]; if (!polygonPointsCurrent) continue; // Determine which points to use based on increaseAmount const step = Math.max(1, Math.floor(polygonPointsCurrent.length / increaseAmount)); // Keep track of new points to add after processing const newPointsToAdd = []; // Create lines for selected points for (let j = 0; j < increaseAmount; j++) { const pointIndex = j * step; if (pointIndex >= polygonPointsCurrent.length) break; // Get current point and next point const currentPoint = polygonPointsCurrent[pointIndex]; const nextPointIndex = (pointIndex + 1) % polygonPointsCurrent.length; const nextPoint = polygonPointsCurrent[nextPointIndex]; // Calculate midpoint between current and next point const midX = (currentPoint[0] + nextPoint[0]) / 2; const midY = (currentPoint[1] + nextPoint[1]) / 2; // Find corresponding point on outermost polygon const outerPolygonPoints = polygonPoints[`polygon-${rows-1}`]; let closestOuterPoint = outerPolygonPoints[0]; let minDistance = Number.MAX_VALUE; // Find the closest point on the outer polygon for (let k = 0; k < outerPolygonPoints.length; k++) { const outerPoint = outerPolygonPoints[k]; const distance = Math.sqrt( Math.pow(outerPoint[0] - midX, 2) + Math.pow(outerPoint[1] - midY, 2) ); if (distance < minDistance) { minDistance = distance; closestOuterPoint = outerPoint; } } // Create line from midpoint to closest outer point const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); line.classList.add("increaseLine"); line.setAttribute("stroke", "red"); line.setAttribute("stroke-width", borderWidth); line.setAttribute("x1", midX); line.setAttribute("y1", midY); line.setAttribute("x2", closestOuterPoint[0]); line.setAttribute("y2", closestOuterPoint[1]); workspace.appendChild(line); // Add the midpoint as a new point with decimal naming const decimalName = pointIndex + 0.5; newPointsToAdd.push({ index: pointIndex + 1, point: [midX, midY], decimalName: decimalName }); // Store point data with decimal naming polygonPointData[`polygon-${i}`][decimalName] = { x: midX, y: midY, isIncrement: true }; } // Add new points to the polygon in order to maintain sequence let insertedCount = 0; for (const newPoint of newPointsToAdd) { polygonPoints[`polygon-${i}`].splice(newPoint.index + insertedCount, 0, newPoint.point); insertedCount++; } // Update the polygon with new points const polygonElements = document.querySelectorAll(".dynamicPolygon"); if (polygonElements[i]) { const pointsString = polygonPoints[`polygon-${i}`].map(point => point.join(", ")).join(" "); polygonElements[i].setAttribute("points", pointsString); } } } // Reset view to default (centered at origin) function resetView() { viewState = { x: originalViewBox.x, y: originalViewBox.y, w: originalViewBox.w, h: originalViewBox.h, isPanning: false, startPoint: { x: 0, y: 0 } }; workspace.setAttribute("viewBox", `${viewState.x} ${viewState.y} ${viewState.w} ${viewState.h}`); zoomValue.textContent = "100%"; } // Apply preset settings function applyPreset(tab) { const preset = presets[tab]; rowsSlider.value = preset.rows; rowsValue.textContent = `${preset.rows}`; rowsDisplay.textContent = `${preset.rows}`; colsSlider.value = preset.cols; colsValue.textContent = `${preset.cols}`; colsDisplay.textContent = `${preset.cols}`; increaseRowsSlider.value = preset.increaseRows; increaseRowsValue.textContent = `${preset.increaseRows}`; increaseRowsDisplay.textContent = `${preset.increaseRows}`; increaseAmountSlider.value = preset.increaseAmount; increaseAmountValue.textContent = `${preset.increaseAmount}`; increaseAmountDisplay.textContent = `${preset.increaseAmount}`; borderSlider.value = preset.borderWidth; borderValue.textContent = `${preset.borderWidth}px`; borderDisplay.textContent = `${preset.borderWidth}px`; updatePolygons(); } // Helper function to convert screen coordinates to SVG coordinates function getSVGPoint(e) { const p = workspace.createSVGPoint(); p.x = e.clientX; p.y = e.clientY; return p.matrixTransform(workspace.getScreenCTM().inverse()); } // Set up event listeners function setupEventListeners() { // Tab switching tabs.forEach(tab => { tab.addEventListener("click", () => { // Update active tab tabs.forEach(t => t.classList.remove("active")); tab.classList.add("active"); // Apply preset currentTab = tab.dataset.tab; applyPreset(currentTab); }); }); // Rows slider input rowsSlider.addEventListener("input", () => { const value = parseInt(rowsSlider.value); presets[currentTab].rows = value; rowsValue.textContent = `${value}`; rowsDisplay.textContent = `${value}`; updatePolygons(); }); // Cols slider input colsSlider.addEventListener("input", () => { const value = parseInt(colsSlider.value); presets[currentTab].cols = value; colsValue.textContent = `${value}`; colsDisplay.textContent = `${value}`; updatePolygons(); }); // Increase rows slider input increaseRowsSlider.addEventListener("input", () => { const value = parseInt(increaseRowsSlider.value); presets[currentTab].increaseRows = value; increaseRowsValue.textContent = `${value}`; increaseRowsDisplay.textContent = `${value}`; updatePolygons(); }); // Increase amount slider input increaseAmountSlider.addEventListener("input", () => { const value = parseInt(increaseAmountSlider.value); presets[currentTab].increaseAmount = value; increaseAmountValue.textContent = `${value}`; increaseAmountDisplay.textContent = `${value}`; updatePolygons(); }); // Border width slider input borderSlider.addEventListener("input", () => { const value = parseInt(borderSlider.value); presets[currentTab].borderWidth = value; borderValue.textContent = `${value}px`; borderDisplay.textContent = `${value}px`; updatePolygons(); }); // Reset button resetBtn.addEventListener("click", resetView); // Toggle panel togglePanel.addEventListener("click", () => { container.classList.toggle("collapsed"); }); // Zoom with mouse wheel workspace.addEventListener("wheel", (e) => { e.preventDefault(); // Determine zoom factor const zoomFactor = e.deltaY < 0 ? 0.9 : 1.1; // Don\'t let it get too small or large if (viewState.w * zoomFactor > 5000 || viewState.w * zoomFactor < 100) return; // Get the mouse position relative to the SVG internal space const mouseSVGPoint = getSVGPoint(e); // Calculate new width and height const newW = viewState.w * zoomFactor; const newH = viewState.h * zoomFactor; // Adjust X and Y so the mouse pointer remains the anchor center of the zoom const newX = mouseSVGPoint.x - (mouseSVGPoint.x - viewState.x) * zoomFactor; const newY = mouseSVGPoint.y - (mouseSVGPoint.y - viewState.y) * zoomFactor; // Update state and DOM viewState.x = newX; viewState.y = newY; viewState.w = newW; viewState.h = newH; workspace.setAttribute("viewBox", `${viewState.x} ${viewState.y} ${viewState.w} ${viewState.h}`); zoomValue.textContent = `${Math.round(1000 / viewState.w * 100)}%`; }); // Pan with middle mouse button workspace.addEventListener("pointerdown", (e) => { if (e.button === 1) { // Middle mouse button viewState.isPanning = true; // Store the exact SVG coordinate where the user clicked viewState.startPoint = getSVGPoint(e); workspace.style.cursor = "grabbing"; e.preventDefault(); } }); workspace.addEventListener("pointermove", (e) => { if (!viewState.isPanning) return; // Get the current mouse position in SVG coordinates const currentPoint = getSVGPoint(e); // Calculate how much the mouse moved inside the SVG space const dx = viewState.startPoint.x - currentPoint.x; const dy = viewState.startPoint.y - currentPoint.y; // Shift the viewBox by the delta movement viewState.x += dx; viewState.y += dy; workspace.setAttribute("viewBox", `${viewState.x} ${viewState.y} ${viewState.w} ${viewState.h}`); }); workspace.addEventListener("pointerup", () => { if (viewState.isPanning) { viewState.isPanning = false; workspace.style.cursor = "grab"; } }); workspace.addEventListener("pointerleave", () => { if (viewState.isPanning) { viewState.isPanning = false; workspace.style.cursor = "grab"; } }); // Handle window resize window.addEventListener("resize", () => { updatePolygons(); }); } // Initialize when page loads window.addEventListener("DOMContentLoaded", initWorkspace); </script></body></html>';
// Output the HTMLecho $html;?>
Imagine knitting with the last warm light of a summer evening — deep, moody hues that shift and glow as your project grows. Each skein is hand-dyed in small batches in South Australia.
This colourway is part of a 5-colour fade-pack — buy the full set or individual skeins. See all KFTG&G colourways and bases →
Yarn Specifications
Weight: 4ply / Fingering
Fibre: Singlespun 60% Superwash Merino, 20% Baby Alpaca, 5% Grade A Mullberry Silk
Yardage: 460m per 115g skein
Dyed in: South Australia, Australia
Frequently Asked Questions
What is KFTG? Knit for the Girls is an annual fibre arts community fundraiser raising money for breast cancer research. Skein Machine has partnered with Louie and Lola Yarns to extend donations to the Prostate Cancer Foundation of Australia — so we Knit for the Girls and Guys.
The Yarn? Yes — 60% Superwash Merino makes it machine-washable and soft against the skin, blended with 20% Alpaca and 20% Grade A Mulberry Silk to give a great drape to your project.
Can I buy just one skein from the fade? Absolutely — individual skeins are available. The full 5-colour fade-pack is also available on the KFTG Skein Machine page. This listing is for one skein.
Where is this yarn made? Hand-dyed in South Australia by Skein Machine.
Sign in as a wholesale client to view pricing and order.