// SinzetaBolt — a parametric S-rayo that breaks out of the containing circle. // // Key design principle from the user's S-Id reference: // - The shape is an "S tipo rayo": two parallelogram strokes joined by a // diagonal slash. Mirror it horizontally and it reads as Z. // - NO arrow heads. Terminations are cuts / hooks / bevels — part of the // stroke language, not added decorations. // - The bolt OVERFLOWS the circle. Circle sits BEHIND. // // Geometry (100x100 logical space, viewBox is larger so bolt can overflow): // The bolt is built from 3 parallelogram-like segments: // top segment — parallelogram going down-left (angle θ from vertical) // middle slash — diagonal connector // bottom segment — parallelogram going down-left (parallel to top) // // Parameters: // angle — slant angle of the parallelograms (12..28 degrees) // thickness — stroke thickness // topEnd — shape of top termination: 'flat-h','flat-v','diag-fw','diag-bk','chisel','point','hook','notch','bevel','square' // botEnd — same options for bottom termination // length — total vertical span (controls how far it overflows the circle) // Build the bolt as an SVG path. Uses manual point-array construction so we can // custom-shape each terminus. function buildBolt({ angle = 20, // slant in degrees (each parallelogram leans this much) thick = 42, // stroke thickness length = 280, // total vertical height of the mark topEnd = 'diag-fw', botEnd = 'diag-fw', mid = 0.5, // where the middle slash sits vertically (0..1) midThick = null, // override thickness of mid slash midAngle = 55, // angle of the middle diagonal slash slashOffset = 0, // horizontal offset of the slash (0=centered) cx = 0, cy = 0, } = {}) { // Center coords. The bolt is centered on (cx, cy) and extends +/- length/2 in y. // Each parallelogram leans 'angle' degrees from vertical. // θ = angle in radians; horizontal offset per unit vertical = tan(θ) const θ = (angle * Math.PI) / 180; const tan = Math.tan(θ); const h = length; const topY = cy - h / 2; const botY = cy + h / 2; const midY = cy - h / 2 + h * mid; const midH = (midThick ?? thick) * 1.0; // The bolt's "spine" is a vertical zigzag. Because the parallelograms lean, // their center lines are also slanted. // // Top parallelogram: from (cx + (h/4)*tan, topY) → (cx - (h/4)*tan, midY - midH/2) // Wait — we want it to LEAN right-to-left going DOWN (so it lines up with // the S shape). Top starts upper-RIGHT, goes down-LEFT. Let's define: // // T1 (top-right-upper) = (cx + offset + halfW*cos, topY) where // offset = h/4 * tan (roughly) // // Simpler: define the CENTER LINE of each segment as two points: // topCenter: A (cx + ax, topY) → B (cx + bx, midY - gap/2) // midSlash: B' (cx + bx, midY) → C' (cx + cx', midY) -- horizontal-ish // botCenter: D (cx + dx, midY + gap/2) → E (cx + ex, botY) // // For the classic S-rayo, top and bottom are PARALLEL, slashed by the mid. // Positions relative to cx: // Top starts upper-RIGHT: ax = +h*0.22*tan (tan is slope) // Top ends lower-LEFT: bx = -h*0.10*tan // Bot starts upper-RIGHT: dx = +h*0.10*tan // Bot ends lower-LEFT: ex = -h*0.22*tan // With the mid slash bridging bx → dx. // // We want visual continuity: the mid slash is steep, going from where top // ends on the LEFT to where bot starts on the RIGHT. // // Actually re-examining the reference ref-s.svg: it's a parallelogram-based // S where the top and bottom bars are slanted LEFTward going DOWN, and a // diagonal bridge connects them. // Simpler model — center line = 4-point polyline, thickened via stroke: // Points (relative to cx, cy, going top→bottom): // P1 = top-right (slightly high-right) // P2 = top-bar-end (center-ish, a bit left) // P3 = bottom-bar-start (center-ish, a bit right) // P4 = bottom-left (low-left) // The middle slash P2→P3 crosses the center. const ax = (h * 0.42) * tan; // horizontal offset at TOP const bx = -(h * 0.08) * tan; // horizontal offset at end of top bar const dx = (h * 0.08) * tan; // horizontal offset at start of bot bar const ex = -(h * 0.42) * tan; // horizontal offset at BOTTOM const P1 = [cx + ax + slashOffset, topY]; const P2 = [cx + bx + slashOffset, midY - (midH * 0.22)]; const P3 = [cx + dx + slashOffset, midY + (midH * 0.22)]; const P4 = [cx + ex + slashOffset, botY]; // Now to render the bolt as a filled polygon (not a stroke) so we can // customize each cap independently, we trace the OUTLINE clockwise. // // For each segment we need a LEFT edge and RIGHT edge offset by thick/2 // perpendicular to that segment's direction. // // helper: offset a line-segment into two parallel edges const offsetSeg = (A, B, d) => { const dxv = B[0] - A[0], dyv = B[1] - A[1]; const L = Math.hypot(dxv, dyv); const nx = -dyv / L, ny = dxv / L; // left-normal return { left: [[A[0] + nx * d, A[1] + ny * d], [B[0] + nx * d, B[1] + ny * d]], right: [[A[0] - nx * d, A[1] - ny * d], [B[0] - nx * d, B[1] - ny * d]], dir: [dxv / L, dyv / L], normal: [nx, ny], }; }; const half = thick / 2; const seg1 = offsetSeg(P1, P2, half); // top bar const seg2 = offsetSeg(P2, P3, half); // mid slash const seg3 = offsetSeg(P3, P4, half); // bottom bar // Line-line intersection const lineIntersect = ([a1, a2], [b1, b2]) => { const x1 = a1[0], y1 = a1[1], x2 = a2[0], y2 = a2[1]; const x3 = b1[0], y3 = b1[1], x4 = b2[0], y4 = b2[1]; const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); if (Math.abs(denom) < 1e-9) return [(x2 + x3) / 2, (y2 + y3) / 2]; const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom; return [x1 + t * (x2 - x1), y1 + t * (y2 - y1)]; }; // Joint points (miter joins between segments) const jL12 = lineIntersect(seg1.left, seg2.left); const jR12 = lineIntersect(seg1.right, seg2.right); const jL23 = lineIntersect(seg2.left, seg3.left); const jR23 = lineIntersect(seg2.right, seg3.right); // Termination points — depends on style // Start with the two "raw" cap points for each end (left/right edges) // topCap: seg1.left[0] and seg1.right[0] // botCap: seg3.left[1] and seg3.right[1] const capEnd = (style, A, B, dir) => { // A is one edge endpoint, B is the other; dir is segment direction (A→B is going further) // We return an array of points to insert between B and A when tracing outline. // For a 'flat-perp' cap, it's just [] (straight line from B to A). // For 'diag-fw' (forward diagonal) one corner is extended along dir. const extend = (pt, t) => [pt[0] + dir[0] * t, pt[1] + dir[1] * t]; const [dx_, dy_] = dir; switch (style) { case 'flat': return []; case 'diag-fw': // extend the LEFT side (A) forward → creates cut going up-right return [extend(A, thick * 0.55)]; case 'diag-bk': // extend the RIGHT side (B) forward return [extend(B, thick * 0.55)]; case 'chisel': // both extended symmetrically forming a point return [extend([(A[0]+B[0])/2, (A[1]+B[1])/2], thick * 0.42)]; case 'point': // full point, long return [extend([(A[0]+B[0])/2, (A[1]+B[1])/2], thick * 0.85)]; case 'hook-fw': // forward hook — A stays, B extends then cuts back return [ extend(B, thick * 0.5), [B[0] + dir[0] * thick * 0.5 - (-dir[1]) * thick * 0.35, B[1] + dir[1] * thick * 0.5 - ( dir[0]) * thick * 0.35], ]; case 'notch': // notched cap — A→midNotch→B (triangle inward) const mid = [(A[0]+B[0])/2, (A[1]+B[1])/2]; return [ extend(A, thick * 0.3), extend(mid, -thick * 0.15), extend(B, thick * 0.3), ]; case 'bevel-fw': return [extend(A, thick * 0.3), extend(B, thick * 0.3)]; case 'bevel-bk': return [extend(A, thick * 0.1), extend(B, thick * 0.1)]; case 'square': // full square extension return [extend(A, thick * 0.5), extend(B, thick * 0.5)]; case 'round-end': // approximate with an arc-ish polygon return Array.from({length: 5}, (_, i) => { const t = (i + 1) / 6; const mid = [(A[0]+B[0])/2, (A[1]+B[1])/2]; const px = -dir[1], py = dir[0]; const angle = -Math.PI/2 + Math.PI * t; return [ mid[0] + dir[0] * Math.cos(angle) * thick * 0.55 + px * Math.sin(angle) * thick * 0.55 * 0, mid[1] + dir[1] * Math.cos(angle) * thick * 0.55 + py * Math.sin(angle) * thick * 0.55 * 0, ]; }); default: return []; } }; // Trace outline clockwise starting at top-left of top cap: // seg1.left[0] → [top cap points] → seg1.right[0] → jR12 → jR23 → seg3.right[1] // → [bot cap points] → seg3.left[1] → jL23 → jL12 → seg1.left[0] const topDir = [-seg1.dir[0], -seg1.dir[1]]; // direction OUT of the top cap (backwards along seg) const botDir = [seg3.dir[0], seg3.dir[1]]; // direction OUT of bottom cap const topPts = capEnd(topEnd, seg1.left[0], seg1.right[0], topDir); const botPts = capEnd(botEnd, seg3.right[1], seg3.left[1], botDir); const outline = [ seg1.left[0], ...topPts, seg1.right[0], jR12, jR23, seg3.right[1], ...botPts, seg3.left[1], jL23, jL12, ]; return { polygon: outline.map(p => p.join(',')).join(' '), anchors: { P1, P2, P3, P4 }, bounds: [topY, botY], }; } // Wrapper component function SinzetaBolt({ params = {}, mode = 'plasma', fill = null, stroke = '#00E5FF', id = 'bolt', mirror = false, // mirror horizontally → reads as Z }) { const b = buildBolt({ cx: 100, cy: 100, ...params }); const gid = (n) => `${id}-${n}`; const transform = mirror ? 'translate(200 0) scale(-1 1)' : ''; const defs = ( ); return ( {defs} {mode === 'plasma' && ( <> {/* outer halo */} {/* inner highlight — drawn inset via smaller scale */} )} {mode === 'solid' && ( )} {mode === 'flat' && } {mode === 'outline' && ( )} {mode === 'neon' && ( <> )} ); } // Main component: circle BEHIND, bolt IN FRONT, bolt overflows. function SinzetaV3({ size = 300, params = {}, mode = 'plasma', id = 'sv3', showCircle = true, showStars = true, circleStroke = 'rgba(0,229,255,0.4)', circleR = 70, // circle radius in 200-unit space (smaller than bolt) circleFill = null, mirror = false, showGuideStar = false, }) { // We render in a 200x200 viewBox but allow overflow via viewBox padding. // Actually: use a larger viewBox so bolt doesn't clip. const vbSize = 260; // larger than 200 to allow overflow const off = (vbSize - 200) / 2; // offset so 200x200 is centered in vb const gid = (n) => `${id}-${n}`; return ( {showCircle && ( <> {showStars && ( )} {showGuideStar && ( )} {circleStroke && ( )} )} ); } Object.assign(window, { SinzetaV3, SinzetaBolt, buildBolt });