// 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 (
);
}
Object.assign(window, { SinzetaV3, SinzetaBolt, buildBolt });