import solara
index = solara.reactive(0)
# HTML+JS for the compass visualization
# Double curly braces are for .format() compatibility if needed elsewhere (not required if no Python formatting).
testing_html = """
<div style="text-align: center;">
<svg id="mysvg" aria-label="Compass" role="img" width="320" height="320" style="margin-bottom: 1em"></svg>
<div id="compass-label" style="font-family: Comfortaa, Montserrat, sans-serif; font-weight: bold; font-size: 1.6em; margin-top: 0.5em"></div>
</div>
<script>
(function() {
// --- Static config
const arcLabels = ["<tspan font-style='italic'>V</tspan>alidation", "Expectation", "Recalibration", "<tspan font-style='italic'>A</tspan>ction"];
const SCALE = 0.8, BASE = 400 * SCALE, NEEDLE = 200 * SCALE;
const NEEDLE_X = 102 * SCALE, NEEDLE_Y = 104 * SCALE;
const PIVOT_X = NEEDLE_X + 0.49 * NEEDLE, PIVOT_Y = NEEDLE_Y + 0.49 * NEEDLE;
const ARC_RADIUS = NEEDLE * 0.5;
// --- Animation params
const firstOver = [47, 49], firstSettle = [2, 4], mainOver = [94, 98], mainSettle = [4, 8];
const OVER_MS = 1000, SETTLE_MS = 500;
// --- State
let arcQuarter = 0, needleAngle = 0, initialized = false, prevLabel = "";
let animating = false, autoMoveTimeout = null;
// --- Drawing helpers
function describeArc(cx, cy, r, q) {
const s = -135 + 90 * q, e = s + 90, rad = Math.PI/180;
const x1 = cx + r * Math.cos(s * rad), y1 = cy + r * Math.sin(s * rad);
const x2 = cx + r * Math.cos(e * rad), y2 = cy + r * Math.sin(e * rad);
return `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 0 1 ${x2} ${y2} Z`;
}
function labelTspan(label) {
// Pass HTML <tspan> as string
if (label === "Validation") return "<tspan font-style='italic'>V</tspan><tspan>alidation</tspan>";
if (label === "Action") return "<tspan font-style='italic'>A</tspan><tspan>ction</tspan>";
return `<tspan>${label}</tspan>`;
}
// --- SVG rendering
function renderSVG(needleAng, q) {
const arcPath = describeArc(PIVOT_X, PIVOT_Y, ARC_RADIUS, q);
const label = arcLabels[q];
const labelX = BASE * 0.02, labelY = BASE * 0.99, fs = BASE * 0.05;
const dx = -fs * 0.02, dy = fs * 0.02;
let svg = document.getElementById("mysvg");
if (!svg) return;
svg.setAttribute('viewBox', `0 0 ${BASE} ${BASE}`);
svg.innerHTML = `
<image href="/files/AIPHeX/solara-svg-compass/VERAcompass.svg" x="0" y="0" width="${BASE}" height="${BASE}" />
<path d="${arcPath}" fill="rgba(45,172,227,1)" stroke-width="0" />
<g id="needle-group" transform="rotate(${needleAng},${PIVOT_X},${PIVOT_Y})">
<image href="/files/AIPHeX/solara-svg-compass/VERAcompass_needle_new.svg"
x="${NEEDLE_X}" y="${NEEDLE_Y}" width="${NEEDLE}" height="${NEEDLE}"/>
</g>
<image href="/files/AIPHeX/solara-svg-compass/VERA_compass_pin.svg"
x="${0.45*BASE}" y="${0.45*BASE}" width="${0.1*BASE}" height="${0.1*BASE}" />
<g>
<text x="${labelX + dx}" y="${labelY + dy}" text-anchor="start"
font-family="'Comfortaa', 'Montserrat', sans-serif" font-size="${fs}" font-weight="bold"
stroke="#000" stroke-width="${fs*0.13}" fill="none" opacity="0.55"
style="pointer-events:none;user-select:none;">
${label}
</text>
<text x="${labelX}" y="${labelY}" text-anchor="start"
font-family="'Comfortaa', 'Montserrat', sans-serif" font-size="${fs}" fill="#000" font-weight="bold"
style="pointer-events:none;user-select:none;">
${label}
</text>
</g>
`;
// Also set text for screen readers
const textDiv = document.getElementById("compass-label");
if (textDiv) textDiv.innerHTML = arcLabels[q].replace(/<[^>]+>/g,"");
}
// --- Animations
function easeInOut(t) { return t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t+2,2)/2;}
function rand([min,max]) { return min + Math.random()*(max-min); }
function animateNeedle(from, to, dur, cb) {
const start = performance.now();
function a(now) {
const t = Math.min((now - start) / dur, 1), v = easeInOut(t);
renderSVG(from + (to-from) * v, arcQuarter);
if (t < 1) requestAnimationFrame(a);
else { renderSVG(to, arcQuarter); if (cb) cb(); }
}
requestAnimationFrame(a);
}
function moveToIndexSpringy(idx, isFirstMove) {
if (animating) return;
let delta = ((idx - arcQuarter + 4) % 4);
if (delta === 0 && isFirstMove) delta = 1; // always move once on autoplay/reset
let over, settle, angDelta;
if (isFirstMove) {
over = firstOver; settle = firstSettle; angDelta = 45 * delta;
initialized = true;
} else {
over = mainOver; settle = mainSettle; angDelta = 90 * delta;
}
const overshoot = angDelta + (rand(over) - 90);
const angleAfter = needleAngle + overshoot;
const angleFinal = needleAngle + angDelta;
animating = true;
arcQuarter = (arcQuarter + delta) % 4;
renderSVG(needleAngle, arcQuarter);
animateNeedle(needleAngle, angleAfter, OVER_MS, () =>
animateNeedle(angleAfter, angleFinal, SETTLE_MS, () => {
needleAngle = angleFinal;
animating = false;
})
);
}
function setCompassIndex(idx, isReset, isAutoMove) {
if (autoMoveTimeout) clearTimeout(autoMoveTimeout);
if (isReset) {
arcQuarter = 0; needleAngle = 0; initialized = false; renderSVG(needleAngle, arcQuarter);
autoMoveTimeout = setTimeout(() => {
moveToIndexSpringy(1, true);
}, 1000);
return;
}
if (isAutoMove) {
moveToIndexSpringy(idx, true);
return;
}
let targetIdx = ((idx % 4) + 4) % 4;
let step = (targetIdx - arcQuarter + 4) % 4;
if (step === 0) return;
moveToIndexSpringy(targetIdx, false);
}
// --- Main observer for data-index attribute
function observeIndexAttribute() {
const parent = document.currentScript.parentElement;
function update() {
const idxRaw = parent.getAttribute("data-index");
if (idxRaw === "reset") {
setCompassIndex(0, true, false);
} else if (idxRaw === "auto") {
setCompassIndex(1, false, true);
} else {
const idx = parseInt(idxRaw);
setCompassIndex(idx, false, false);
}
}
// Initial paint
arcQuarter = 0; needleAngle = 0; initialized = false; renderSVG(needleAngle, arcQuarter);
// Observe attribute
const observer = new MutationObserver(update);
observer.observe(parent, { attributes: true, attributeFilter: ["data-index"] });
// Autoplay after load
setTimeout(() => { parent.setAttribute("data-index", "auto"); }, 1000);
// Also update on load
update();
}
// Only run once per page load
if (!window._solara_compass_loaded) {
window._solara_compass_loaded = true;
observeIndexAttribute();
}
})();
</script>
"""
@solara.component
def Page():
def go_prev():
index.value = (index.value - 1) % 4
def go_next():
index.value = (index.value + 1) % 4
def do_reset():
# Setting data-index to "reset"
index.value = 0 # triggers attribute update to 0, then we set to "reset" on click
solara.util.schedule(lambda: index.set("reset")) # forces attribute update for observer
# The data-index attribute must be a string ("reset", "auto", or an integer 0-3)
if isinstance(index.value, int):
data_index = str(index.value)
else:
data_index = index.value
solara.HTML(
tag="div",
unsafe_innerHTML=testing_html,
attributes={"data-index": data_index}
)
with solara.Row():
solara.Button("◀ Prev", on_click=go_prev)
solara.Button("Next ▶", on_click=go_next)
solara.Button("Reset", on_click=lambda: solara.util.schedule(lambda: index.set("reset")))
solara.Select(label="State", values=[0, 1, 2, 3], value=index.value if isinstance(index.value, int) else 0, on_value=lambda v: index.set(v))