47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
16:45DeepSeek V4.1 Flash Is Shockingly Good for the Price
Sep 10, 2026 · YouTube
17:14How much better is GPT-6 Astra over Claude Opus 5? Real Website and Coding Tests
Sep 5, 2026 · YouTube
29:13Implementing a multiplayer game with GPT-6 Astra (+ Gemini 3.8, Opus 5, Sol Pro)
Sep 5, 2026 · YouTube
19:42Meta Muse Spark 1.3 Review: A Huge Leap Forward
Sep 2, 2026 · YouTube
17:31Testing Gemini 3.8 Flash on Real Coding Challenges (vs. other models)
Sep 2, 2026 · YouTube
Build one self-contained HTML page for a museum armoury exhibit: a standing figure and a slider that re-equips that same figure across three periods of human history.
Choose any three periods from recorded human history, anywhere in the world. Put them in chronological order and keep adjacent choices at least 100 years apart. For each, name a specific year and place; if an exact year is impossible, give the tightest defensible date range and explain it in the label.
Choose periods you can depict accurately. You are judged on the drawing as well as the interface.
Math.random, clocks, or accumulated state.localStorage, sessionStorage, cookies, or indexedDB.One line per period: date, place, equipment, weapon, and one specific detail a non-specialist might miss.
One paragraph explaining the attachment points, layer order, and interpolation between periods.
One fenced html block containing the complete page.
Name the least certain historical choice and any accuracy compromise you made for legibility.
I will move the slider slowly in both directions, watch the feet and shoulders for drift, compare labels to drawings, inspect the SVG structure, and go straight to the final period before judging the rest.
This is a self-contained HTML page for a museum armoury exhibit. It places a single standing figure on a slider, morphing armour and weapon across Roman, Norman, and Milanese periods while keeping the same body rig.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Armoury · Three Answers to the Same Question</title>
<style>
:root{
--bg:#0d1015; --panel:#161a22; --ink:#e9e3d7; --dim:#98a2b0;
--gold:#c8a45c; --line:#2a303b;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
background:radial-gradient(1100px 620px at 50% -8%, #1b212c 0%, #0c0f14 68%);
color:var(--ink);
font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,"Times New Roman",serif;
min-height:100vh;
}
main{max-width:900px;margin:0 auto;padding:26px 18px 70px}
header{text-align:center;margin-bottom:6px}
h1{font-size:clamp(19px,3.3vw,29px);letter-spacing:.02em;margin:0 0 7px;font-weight:500}
header p{margin:0;color:var(--dim);font-size:14px;font-style:italic}
.stage{display:flex;justify-content:center;margin-top:2px}
svg{width:100%;max-width:540px;height:auto;display:block;overflow:visible}
.controls{margin:6px auto 0;max-width:560px}
input[type=range]{
-webkit-appearance:none;appearance:none;width:100%;height:34px;background:transparent;cursor:pointer;
}
input[type=range]::-webkit-slider-runnable-track{
height:4px;border-radius:2px;background:linear-gradient(90deg,#3d4657,#5b6578);
}
input[type=range]::-moz-range-track{height:4px;border-radius:2px;background:#3d4657}
input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:22px;height:22px;border-radius:50%;margin-top:-9px;
background:radial-gradient(circle at 34% 30%,#f3e6c8,#c8a45c 62%,#8d6f31);
border:1px solid #6f5827;box-shadow:0 2px 8px rgba(0,0,0,.6);
}
input[type=range]::-moz-range-thumb{
width:20px;height:20px;border-radius:50%;border:1px solid #6f5827;
background:radial-gradient(circle at 34% 30%,#f3e6c8,#c8a45c 62%,#8d6f31);
box-shadow:0 2px 8px rgba(0,0,0,.6);
}
.ticks{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dim);
letter-spacing:.09em;text-transform:uppercase;margin-top:-2px;padding:0 2px}
.ticks span{width:33.33%;text-align:center}
.ticks span:first-child{text-align:left}
.ticks span:last-child{text-align:right}
.label{
margin:24px auto 0;max-width:640px;background:linear-gradient(180deg,#191e27,#12161d);
border:1px solid var(--line);border-radius:3px;padding:20px 24px 22px;
box-shadow:0 18px 40px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.03);
position:relative;
}
.label::before{
content:"";position:absolute;left:24px;right:24px;top:0;height:2px;
background:linear-gradient(90deg,transparent,var(--gold),transparent);opacity:.6;
}
.lab-kicker{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--gold);margin-bottom:7px}
.lab-title{font-size:clamp(17px,2.6vw,22px);margin:0 0 3px;font-weight:500}
.lab-date{color:var(--dim);font-size:14px;margin-bottom:14px}
.row{display:grid;grid-template-columns:96px 1fr;gap:10px;font-size:13.6px;line-height:1.5;padding:5px 0;border-top:1px solid #1e242e}
.row:first-of-type{border-top:none}
.row b{font-weight:400;color:var(--gold);font-size:10.6px;letter-spacing:.13em;text-transform:uppercase;
padding-top:3px;display:block}
.row div{color:#d8d2c6}
.note{margin-top:14px;padding-top:12px;border-top:1px solid #1e242e;font-size:13px;color:var(--dim);font-style:italic;line-height:1.55}
.blend{display:flex;height:5px;border-radius:3px;overflow:hidden;margin:12px 0 4px;border:1px solid #262c37}
.blend i{display:block;height:100%}
.blend i.a{background:#7c6a3e}
.blend i.b{background:#b9c4d2}
.blendcap{display:flex;justify-content:space-between;font-size:11px;color:var(--dim);letter-spacing:.05em}
@media (max-width:520px){
.row{grid-template-columns:1fr;gap:2px}
.row b{padding-top:0}
}
</style>
</head>
<body>
<main>
<header>
<h1>Three Answers to the Same Question</h1>
<p>One body, one rig — re-equipped across eleven centuries.</p>
</header>
<div class="stage">
<svg id="stage" viewBox="0 130 470 620" role="img"
aria-label="A standing figure whose armour and weapon change between a Roman legionary, a Norman knight and a Milanese man-at-arms."></svg>
</div>
<div class="controls">
<input id="slider" type="range" min="0" max="200" step="1" value="0"
aria-label="Blend between the three periods">
<div class="ticks">
<span>105 AD</span><span>1100 AD</span><span>1450 AD</span>
</div>
</div>
<div class="label" id="label"></div>
</main>
<script>
(function(){
"use strict";
/* ============================================================
0. MATH + MIXING
============================================================ */
const lerp = (a,b,u)=>a+(b-a)*u;
const hex2rgb = h=>[parseInt(h.slice(1,3),16),parseInt(h.slice(3,5),16),parseInt(h.slice(5,7),16)];
const rgb2hex = (r,g,b)=>'#'+[r,g,b].map(v=>Math.max(0,Math.min(255,Math.round(v))).toString(16).padStart(2,'0')).join('');
const mixHex = (a,b,u)=>{const A=hex2rgb(a),B=hex2rgb(b);
return rgb2hex(lerp(A[0],B[0],u),lerp(A[1],B[1],u),lerp(A[2],B[2],u));};
function mix(a,b,u){
if(u<=0) return a;
if(u>=1) return b;
if(typeof a==='number') return a+(b-a)*u;
if(typeof a==='string') return a.charAt(0)==='#' ? mixHex(a,b,u) : a;
if(Array.isArray(a)) return a.map((v,i)=>mix(v,b[i],u));
const o={};
for(const k in a) o[k]=mix(a[k],b[k],u);
return o;
}
/* ============================================================
1. PATH BUILDERS
============================================================ */
const poly = pts => 'M'+pts.map(p=>p[0].toFixed(1)+','+p[1].toFixed(1)).join('L')+'Z';
function smooth(pts){
const n=pts.length;
const mx=(pts[n-1][0]+pts[0][0])/2, my=(pts[n-1][1]+pts[0][1])/2;
let d='M'+mx.toFixed(1)+','+my.toFixed(1);
for(let i=0;i<n;i++){
const c=pts[i], q=pts[(i+1)%n];
d+='Q'+c[0].toFixed(1)+','+c[1].toFixed(1)+' '+
((c[0]+q[0])/2).toFixed(1)+','+((c[1]+q[1])/2).toFixed(1);
}
return d+'Z';
}
/* capsule = thick round-capped limb segment, as a filled path */
function capsule(a,b,w){
const dx=b[0]-a[0], dy=b[1]-a[1], L=Math.hypot(dx,dy)||1;
const ux=dx/L, uy=dy/L, px=-uy, py=ux, r=w/2;
return 'M'+(a[0]+px*r).toFixed(1)+','+(a[1]+py*r).toFixed(1)+
'L'+(b[0]+px*r).toFixed(1)+','+(b[1]+py*r).toFixed(1)+
'A'+r+','+r+' 0 0 0 '+(b[0]-px*r).toFixed(1)+','+(b[1]-py*r).toFixed(1)+
'L'+(a[0]-px*r).toFixed(1)+','+(a[1]-py*r).toFixed(1)+
'A'+r+','+r+' 0 0 0 '+(a[0]+px*r).toFixed(1)+','+(a[1]+py*r).toFixed(1)+'Z';
}
/* ============================================================
2. FIXED BODY RIG (never changes across periods)
============================================================ */
const HEAD_C = [250,228], HEAD_RX = 26, HEAD_RY = 32;
const NECK_T = [250,252], NECK_B = [250,292];
const SH_L = [206,288], SH_R = [294,288];
const HIP_L = [226,458], HIP_R = [274,458];
const KNEE_L = [232,558], KNEE_R = [268,558];
const ANK_L = [234,664], ANK_R = [266,664];
const GROUND = 692;
const TORSO = [
[206,288],[198,314],[199,352],[204,394],[210,434],[224,458],[250,464],[276,458],
[290,434],[296,394],[301,352],[302,314],[294,288],[276,280],[250,278],[224,280]
];
const MIR = pts => pts.map(p=>[500-p[0], p[1]]);
/* ---- feet (per period) ---- */
const FOOT_ROMAN_L = [[214,658],[209,674],[219,687],[234,691],[252,689],[257,671],[253,656],[232,652]];
const FOOT_NORMAN_L= [[216,656],[207,676],[221,690],[250,691],[260,668],[256,650],[240,646],[222,646]];
const FOOT_PLATE_L = [[213,654],[205,677],[224,691],[257,691],[265,664],[258,646],[239,642],[219,642]];
/* ---- torso layers ---- */
const UNDER_ROMAN = [
[203,286],[194,314],[195,352],[200,394],[206,436],[221,460],[250,466],[279,460],
[294,436],[300,394],[305,352],[306,314],[297,286],[277,277],[250,275],[223,277]
];
const UNDER_NORMAN = [
[200,286],[190,314],[190,352],[195,394],[202,436],[218,462],[250,468],[282,462],
[298,436],[305,394],[310,352],[310,314],[300,286],[278,275],[250,273],[222,275]
];
const UNDER_PLATE = [
[208,288],[200,316],[201,354],[205,394],[211,432],[225,456],[250,462],[275,456],
[289,432],[295,394],[299,354],[300,316],[292,288],[275,280],[250,278],[225,280]
];
const ARM_ROMAN = [
[202,284],[192,312],[193,352],[198,394],[205,436],[220,460],[250,466],[280,460],
[295,436],[302,394],[307,352],[308,312],[298,284],[277,272],[250,270],[223,272]
];
const ARM_PLATE = [
[206,286],[197,314],[198,354],[203,394],[209,432],[224,454],[250,460],[276,454],
[291,432],[297,394],[302,354],[303,314],[294,286],[276,277],[250,275],[224,277]
];
const SKIRT_ROMAN = [
[210,432],[240,430],[260,430],[290,432],
[296,470],[300,520],[302,556],
[280,562],[250,564],[220,562],
[198,556],[200,520],[204,470]
];
const SKIRT_NORMAN = [
[204,430],[238,428],[262,428],[296,430],
[302,480],[306,530],[308,582],
[282,588],[250,590],[218,588],
[192,582],[194,530],[198,480]
];
const SKIRT_PLATE = [
[206,430],[238,428],[262,428],[294,430],
[296,448],[296,462],[294,478],
[272,484],[250,486],[228,484],
[206,478],[204,462],[204,448]
];
/* ---- helmets (16 points each) ---- */
const HELM_ROMAN = [
[219,246],[215,230],[215,214],[218,200],
[226,189],[237,181],[250,178],[263,181],
[274,189],[282,200],[285,214],[285,230],
[281,246],[266,224],[250,216],[234,224]
];
const HELM_NORMAN = [
[220,244],[214,228],[214,210],[220,190],
[230,172],[240,160],[250,154],[260,160],
[270,172],[280,190],[286,210],[286,228],
[280,244],[266,224],[250,216],[234,224]
];
const HELM_PLATE = [
[220,240],[214,226],[212,210],[216,196],
[226,186],[238,180],[250,178],[262,180],
[274,186],[284,196],[288,210],[286,226],
[280,240],[268,238],[250,236],[232,238]
];
const CHEEK_L = [[214,228],[216,248],[226,263],[237,264],[239,244],[234,230]];
const CHEEK_R = MIR(CHEEK_L);
const NASAL = [[244,220],[256,220],[253,252],[247,252]];
const VISOR = [
[228,232],[240,228],[260,228],[272,232],
[280,244],[279,256],[268,266],[250,270],
[232,266],[221,256],[220,244],[222,236]
];
/* ---- shields (13 points each) ---- */
const SHIELD_ROMAN = [
[42,450],[60,434],[196,434],[214,450],
[214,520],[214,590],[214,660],[200,686],
[160,691],[96,691],[56,686],[42,660],[42,555]
];
const SHIELD_NORMAN = [
[66,404],[96,386],[128,380],[160,386],[190,404],
[202,460],[196,530],[180,600],[152,656],
[128,690],[104,656],[80,598],[60,500]
];
/* ============================================================
3. PERIOD SPECS
============================================================ */
const DEFAULTS = {
oArmour:1, oMail:0, oBands:0, oPlate:0,
oCheek:0, oNasal:0, oVisor:0,
oLegArmour:0, oArmArmour:0, oSleeve:0,
oShield:1, shieldScale:1, oBoss:1, oSpine:0,
oFace:1, oFuller:0,
oPoleyn:0, oCouter:0, oPauldron:0, oGauntlet:0,
oBootStrap:0, oBootSeam:0, oBootLame:0, oFauld:0,
cFist:'#c98f66'
};
const spec = o => Object.assign({}, DEFAULTS, o);
const ROMAN = spec({
cUnder1:'#a8493c', cUnder2:'#6d2a24',
cArm1:'#8d95a1', cArm2:'#575e69',
cSkirt1:'#a8493c', cSkirt2:'#6d2a24',
cLimb1:'#c98f66', cLimb2:'#95674a',
cHelm1:'#8d95a1', cHelm2:'#575e69',
cShield1:'#c6903c',cShield2:'#7d5a24',
cBelt:'#6d4527',
cGrip:'#cbbd98', cGuard:'#a9b0b9', cBlade:'#c9d0d8',
oArmour:1, oMail:0, oBands:1, oPlate:0,
oCheek:1, oNasal:0, oVisor:0,
oLegArmour:0, oArmArmour:0, oSleeve:1,
oShield:1, shieldScale:1, oBoss:1, oSpine:1,
oFace:1, oFuller:0,
oBootStrap:1, oBootSeam:0, oBootLame:0, oFauld:0,
cFist:'#c98f66',
torsoUnder:UNDER_ROMAN, torsoArm:ARM_ROMAN,
skirt:SKIRT_ROMAN, helm:HELM_ROMAN, shield:SHIELD_ROMAN,
footL:FOOT_ROMAN_L, footR:MIR(FOOT_ROMAN_L),
elbowL:[190,374], wristL:[178,468],
elbowR:[308,372], wristR:[302,452],
boss:[128,556], bossR:21,
sword:{tilt:10, bladeLen:124, bladeW:16, tipLen:26, tipFrac:0.85,
guardW:44, guardT:9, gripLen:44, pommelRx:11, pommelRy:10}
});
const NORMAN = spec({
cUnder1:'#8e959f', cUnder2:'#5b626c',
cArm1:'#8e959f', cArm2:'#5b626c',
cSkirt1:'#8e959f', cSkirt2:'#5b626c',
cLimb1:'#8e959f', cLimb2:'#5b626c',
cHelm1:'#8d949e', cHelm2:'#575d67',
cShield1:'#8c4a3a',cShield2:'#5a2c22',
cBelt:'#5d3f28',
cGrip:'#6f4c30', cGuard:'#9aa1aa', cBlade:'#cbd2da',
oArmour:0, oMail:1, oBands:0, oPlate:0,
oCheek:0, oNasal:1, oVisor:0,
oLegArmour:1, oArmArmour:1, oSleeve:0,
oShield:1, shieldScale:1, oBoss:1, oSpine:0,
oFace:1, oFuller:1,
oBootStrap:0, oBootSeam:1, oBootLame:0, oFauld:0,
cFist:'#c98f66',
torsoUnder:UNDER_NORMAN, torsoArm:ARM_ROMAN,
skirt:SKIRT_NORMAN, helm:HELM_NORMAN, shield:SHIELD_NORMAN,
footL:FOOT_NORMAN_L, footR:MIR(FOOT_NORMAN_L),
elbowL:[192,374], wristL:[182,470],
elbowR:[308,372], wristR:[302,448],
boss:[128,470], bossR:15,
sword:{tilt:10, bladeLen:208, bladeW:14, tipLen:30, tipFrac:0.5,
guardW:56, guardT:8, gripLen:42, pommelRx:15, pommelRy:8}
});
const MILANESE = spec({
cUnder1:'#4a4055', cUnder2:'#2b2433',
cArm1:'#e2e9f1', cArm2:'#8f98a4',
cSkirt1:'#dbe3ec', cSkirt2:'#8b939e',
cLimb1:'#dfe7ef', cLimb2:'#8b939e',
cHelm1:'#e4ebf3', cHelm2:'#8f98a4',
cShield1:'#8c4a3a',cShield2:'#5a2c22',
cBelt:'#4a3225',
cGrip:'#5d3f28', cGuard:'#c3cbd4', cBlade:'#d5dce4',
oArmour:1, oMail:0, oBands:0, oPlate:1,
oCheek:0, oNasal:0, oVisor:1,
oLegArmour:1, oArmArmour:1, oSleeve:0,
oShield:1, shieldScale:0, oBoss:0, oSpine:0,
oFace:0, oFuller:0,
oPoleyn:1, oCouter:1, oPauldron:1, oGauntlet:1,
oBootStrap:0, oBootSeam:0, oBootLame:1, oFauld:1,
cFist:'#c3cbd4',
torsoUnder:UNDER_PLATE, torsoArm:ARM_PLATE,
skirt:SKIRT_PLATE, helm:HELM_PLATE, shield:SHIELD_NORMAN,
footL:FOOT_PLATE_L, footR:MIR(FOOT_PLATE_L),
elbowL:[196,376], wristL:[210,456],
elbowR:[308,372], wristR:[304,446],
boss:[128,470], bossR:15,
sword:{tilt:11, bladeLen:224, bladeW:12, tipLen:42, tipFrac:0.35,
guardW:62, guardT:7, gripLen:40, pommelRx:13, pommelRy:13}
});
const SPECS = [ROMAN, NORMAN, MILANESE];
const META = [
{ short:'Roman legionary',
title:'Roman legionary',
date:'c. 105 AD',
place:'Lambaesis, Numidia — Roman North Africa',
equip:'Lorica segmentata (Corbridge type); Imperial Gallic helmet with bronze trim and cheek pieces; madder-red wool tunic; hobnailed leather caligae; bronze-plated military belt',
mats:'Iron, brass, leather, wool, linen',
weapon:'Gladius, Pompeii type — iron blade about 450 mm, bone grip, wooden guard',
note:'The banded lorica is not a solid shell: it is separate iron strips fastened to internal leather straps, so it flexes with the wearer.' },
{ short:'Norman knight',
title:'Norman knight',
date:'c. 1100',
place:'Caen, Normandy — Duchy of Normandy',
equip:'Knee-length mail hauberk, split front and back for riding, with mail sleeves to the wrist; mail chausses on the legs; conical spangenhelm with an iron nasal; kite shield of leather-covered limewood with an iron boss and rim',
mats:'Iron wire riveted into mail, iron, leather, limewood, wool',
weapon:'Knightly sword, Oakeshott Type X — iron blade about 780 mm with a wide fuller, Brazil-nut pommel',
note:'The helm has no face plate: the nasal guards only the nose, and the face is otherwise open — while the shield has already stretched from a round to a knee-length kite.' },
{ short:'Milanese man-at-arms',
title:'Milanese man-at-arms',
date:'c. 1450',
place:'Milan, Duchy of Milan — workshop tradition of the Missaglia family',
equip:'Complete "white" (polished, unblued) plate harness: armet with visor and wrapper, gorget, pauldrons, breastplate with plackart and fauld, articulated arms with couters, gauntlets, cuisses, poleyns, greaves and pointed sabatons, worn over an arming doublet',
mats:'Low-carbon iron/steel plate, hardened steel edges, brass rivets, leather straps, linen arming doublet',
weapon:'Arming sword, Oakeshott Type XV — steel blade about 830 mm, sharply tapered diamond section, wheel pommel and curved quillons',
note:'Everything above hangs on the arming doublet: the plates are tied on with laces ("points"), and the lames at elbow, knee and waist are what let a solid-looking shell move.' }
];
/* ============================================================
4. BUILD SVG
============================================================ */
const NS='http://www.w3.org/2000/svg';
const mk=(t,a)=>{const e=document.createElementNS(NS,t); if(a) for(const k in a) e.setAttribute(k,a[k]); return e;};
const svg = document.getElementById('stage');
const defs = mk('defs'); svg.appendChild(defs);
/* --- gradients --- */
function grad(id){
const g=mk('linearGradient',{id:id,x1:'0',y1:'0',x2:'1',y2:'0'});
const a=mk('stop',{offset:'0'}), b=mk('stop',{offset:'1'});
g.appendChild(a); g.appendChild(b); defs.appendChild(g); return [a,b];
}
const gUnder = grad('gUnder');
const gArm = grad('gArm');
const gSkirt = grad('gSkirt');
const gLimb = grad('gLimb');
const gHelm = grad('gHelm');
const gShield= grad('gShield');
/* --- mail pattern --- */
const mailPat = mk('pattern',{id:'mail',width:'9',height:'9',patternUnits:'userSpaceOnUse'});
const mg = mk('g',{fill:'none',stroke:'#e6eef8','stroke-width':'1.6',opacity:'0.5'});
[[4.5,4.5],[0,0],[9,0],[0,9],[9,9]].forEach(c=>mg.appendChild(mk('circle',{cx:c[0],cy:c[1],r:'3.6'})));
mailPat.appendChild(mg); defs.appendChild(mailPat);
/* --- armour clip --- */
const clipArm = mk('clipPath',{id:'clipArm'});
const clipArmPath = mk('path',{}); clipArm.appendChild(clipArmPath); defs.appendChild(clipArm);
/* --- element refs --- */
const E={};
function add(parent, tag, attrs){ const e=mk(tag,attrs); parent.appendChild(e); return e; }
/* 0. shadow */
E.shadow = add(svg,'ellipse',{cx:250,cy:GROUND+4,rx:150,ry:16,fill:'url(#gShadow)'});
const shGrad = mk('radialGradient',{id:'gShadow'});
shGrad.appendChild(mk('stop',{offset:'0','stop-color':'#000','stop-opacity':'0.55'}));
shGrad.appendChild(mk('stop',{offset:'1','stop-color':'#000','stop-opacity':'0'}));
defs.appendChild(shGrad);
/* 1. legs */
const legBodyLD = capsule(HIP_L,KNEE_L,46)+capsule(KNEE_L,ANK_L,34);
const legBodyRD = capsule(HIP_R,KNEE_R,46)+capsule(KNEE_R,ANK_R,34);
const legArmLD = capsule(HIP_L,KNEE_L,54)+capsule(KNEE_L,ANK_L,42);
const legArmRD = capsule(HIP_R,KNEE_R,54)+capsule(KNEE_R,ANK_R,42);
E.legLBody = add(svg,'path',{d:legBodyLD,fill:'url(#gLimb)'});
E.legRBody = add(svg,'path',{d:legBodyRD,fill:'url(#gLimb)'});
E.legLArm = add(svg,'path',{d:legArmLD, fill:'url(#gLimb)'});
E.legRArm = add(svg,'path',{d:legArmRD, fill:'url(#gLimb)'});
E.legLMail = add(svg,'path',{d:legArmLD, fill:'url(#mail)', opacity:0});
E.legRMail = add(svg,'path',{d:legArmRD, fill:'url(#mail)', opacity:0});
/* 2. feet */
E.footL = add(svg,'path',{d:poly(FOOT_ROMAN_L)});
E.footR = add(svg,'path',{d:poly(FOOT_ROMAN_L)});
/* 3. neck */
E.neck = add(svg,'path',{d:capsule(NECK_T,NECK_B,28),fill:'#a97a58'});
/* 4. torso stack */
E.torsoBody = add(svg,'path',{d:smooth(TORSO),fill:'#4c463c'});
E.torsoUnder= add(svg,'path',{d:smooth(UNDER_ROMAN),fill:'url(#gUnder)'});
E.mailTorso = add(svg,'path',{d:smooth(UNDER_ROMAN),fill:'url(#mail)',opacity:0});
E.torsoArm = add(svg,'path',{d:smooth(ARM_ROMAN),fill:'url(#gArm)'});
/* 5. lorica details (clipped) */
const gLorica = mk('g',{'clip-path':'url(#clipArm)'});
for(let i=0;i<9;i++){
const y = 288 + i*19;
gLorica.appendChild(mk('path',{d:'M170,'+y+' Q250,'+(y-7)+' 330,'+y,
fill:'none',stroke:'#39404a','stroke-width':'3.4','stroke-linecap':'round'}));
gLorica.appendChild(mk('path',{d:'M170,'+(y-2.6)+' Q250,'+(y-9.6)+' 330,'+(y-2.6),
fill:'none',stroke:'#b6bec9','stroke-width':'1.2','stroke-linecap':'round',opacity:'0.75'}));
}
gLorica.appendChild(mk('path',{d:'M234,278 L234,452',stroke:'#3a2f24','stroke-width':'4','fill':'none'}));
gLorica.appendChild(mk('path',{d:'M266,278 L266,452',stroke:'#3a2f24','stroke-width':'4','fill':'none'}));
svg.appendChild(gLorica);
E.lorica = gLorica;
E.lorica.style.opacity = 1;
/* 6. plate details (clipped) */
const gPlate = mk('g',{'clip-path':'url(#clipArm)'});
gPlate.appendChild(mk('path',{d:'M250,278 L250,452',fill:'none',stroke:'#ffffff',
'stroke-width':'2.4',opacity:'0.55'}));
gPlate.appendChild(mk('path',{d:'M206,394 Q250,384 294,394',fill:'none',stroke:'#6d7681',
'stroke-width':'2',opacity:'0.8'}));
gPlate.appendChild(mk('path',{d:'M214,432 Q250,424 286,432',fill:'none',stroke:'#6d7681',
'stroke-width':'2',opacity:'0.7'}));
svg.appendChild(gPlate);
E.plate = gPlate;
/* 7. skirt */
E.skirt = add(svg,'path',{d:smooth(SKIRT_ROMAN),fill:'url(#gSkirt)'});
const gFauld = mk('g');
gFauld.appendChild(mk('path',{d:'M208,446 Q250,440 292,446',fill:'none',stroke:'#6d7681','stroke-width':'2','opacity':'0.8'}));
gFauld.appendChild(mk('path',{d:'M208,464 Q250,458 292,464',fill:'none',stroke:'#6d7681','stroke-width':'2','opacity':'0.8'}));
svg.appendChild(gFauld); E.fauld = gFauld;
/* 8. belt */
E.belt = add(svg,'path',{d:'M206,424 Q250,418 294,424 L294,436 Q250,430 206,436 Z',fill:'#6d4527'});
E.buckle = add(svg,'rect',{x:242,y:420,width:16,height:18,rx:3,fill:'#b08b46',stroke:'#7a5c28','stroke-width':'1'});
/* 9. arms */
function buildArm(side){
const sh = side==='L'?SH_L:SH_R;
const el = side==='L'?ROMAN.elbowL:ROMAN.elbowR;
const wr = side==='L'?ROMAN.wristL:ROMAN.wristR;
const bodyD = capsule(sh,el,34)+capsule(el,wr,28);
const armD = capsule(sh,el,42)+capsule(el,wr,34);
const o={};
o.body = add(svg,'path',{d:bodyD,fill:'url(#gLimb)'});
o.sleeve = add(svg,'path',{d:capsule(sh,[(sh[0]+el[0])/2,(sh[1]+el[1])/2],38),fill:'url(#gUnder)'});
o.arm = add(svg,'path',{d:armD,fill:'url(#gLimb)'});
o.mail = add(svg,'path',{d:armD,fill:'url(#mail)',opacity:0});
return o;
}
const armL = buildArm('L');
const armR = buildArm('R');
/* shoulder / elbow plates for the plate harness */
E.pauldronL = add(svg,'circle',{cx:206,cy:292,r:26,fill:'url(#gLimb)',opacity:0});
E.pauldronR = add(svg,'circle',{cx:294,cy:292,r:26,fill:'url(#gLimb)',opacity:0});
E.couterL = add(svg,'circle',{cx:196,cy:376,r:19,fill:'url(#gLimb)',opacity:0});
E.couterR = add(svg,'circle',{cx:308,cy:372,r:19,fill:'url(#gLimb)',opacity:0});
E.poleynL = add(svg,'circle',{cx:232,cy:558,r:23,fill:'url(#gLimb)',opacity:0});
E.poleynR = add(svg,'circle',{cx:268,cy:558,r:23,fill:'url(#gLimb)',opacity:0});
/* left fist (behind the shield) */
E.fistL = add(svg,'circle',{cx:178,cy:468,r:12,fill:'#c98f66'});
/* 10. head + face */
E.head = add(svg,'ellipse',{cx:HEAD_C[0],cy:HEAD_C[1],rx:HEAD_RX,ry:HEAD_RY,fill:'#c98f66'});
const gFace = mk('g',{fill:'#2a2118'});
gFace.appendChild(mk('ellipse',{cx:240,cy:224,rx:3.6,ry:2.6}));
gFace.appendChild(mk('ellipse',{cx:260,cy:224,rx:3.6,ry:2.6}));
gFace.appendChild(mk('path',{d:'M250,228 L250,237',stroke:'#8a5c3c','stroke-width':'2',fill:'none'}));
gFace.appendChild(mk('path',{d:'M243,247 Q250,250 257,247',stroke:'#7d4a30','stroke-width':'2',fill:'none'}));
svg.appendChild(gFace); E.face = gFace;
/* 11. helmet */
E.helm = add(svg,'path',{d:smooth(HELM_ROMAN),fill:'url(#gHelm)'});
const gRoman = mk('g');
gRoman.appendChild(mk('path',{d:poly(CHEEK_L),fill:'url(#gHelm)'}));
gRoman.appendChild(mk('path',{d:poly(CHEEK_R),fill:'url(#gHelm)'}));
gRoman.appendChild(mk('circle',{cx:219,cy:243,r:3,fill:'#b08b46'}));
gRoman.appendChild(mk('circle',{cx:281,cy:243,r:3,fill:'#b08b46'}));
svg.appendChild(gRoman); E.gRoman = gRoman;
const gNorman = mk('g');
gNorman.appendChild(mk('path',{d:poly(NASAL),fill:'#7d848e'}));
gNorman.appendChild(mk('circle',{cx:250,cy:222,r:2.6,fill:'#c8a45c'}));
svg.appendChild(gNorman); E.gNorman = gNorman;
const gMilan = mk('g');
gMilan.appendChild(mk('path',{d:smooth(VISOR),fill:'url(#gHelm)'}));
gMilan.appendChild(mk('rect',{x:234,y:244,width:32,height:3.6,rx:1.8,fill:'#0b0d11'}));
gMilan.appendChild(mk('circle',{cx:238,cy:256,r:2.2,fill:'#0b0d11'}));
gMilan.appendChild(mk('circle',{cx:244,cy:258,r:2.2,fill:'#0b0d11'}));
gMilan.appendChild(mk('circle',{cx:250,cy:259,r:2.2,fill:'#0b0d11'}));
gMilan.appendChild(mk('circle',{cx:221,cy:240,r:3.4,fill:'#9aa3ae'}));
gMilan.appendChild(mk('circle',{cx:279,cy:240,r:3.4,fill:'#9aa3ae'}));
svg.appendChild(gMilan); E.gMilan = gMilan;
/* 12. shield */
const gShield = mk('g');
E.shieldFace = mk('path',{d:poly(SHIELD_ROMAN),fill:'url(#gShield)'});
E.shieldRim = mk('path',{d:poly(SHIELD_ROMAN),fill:'none',stroke:'#3c2c18','stroke-width':'5'});
E.shieldSpine= mk('path',{d:'M121,444 L135,444 L135,684 L121,684 Z',fill:'#b08b46'});
E.shieldBoss = mk('circle',{cx:128,cy:556,r:21,fill:'#c8a45c',stroke:'#7d5c26','stroke-width':'2'});
gShield.appendChild(E.shieldFace);
gShield.appendChild(E.shieldRim);
gShield.appendChild(E.shieldSpine);
gShield.appendChild(E.shieldBoss);
svg.appendChild(gShield); E.gShield = gShield;
/* 13. sword */
const gSword = mk('g');
E.blade = mk('path',{fill:'url(#gHelm)'});
E.fuller= mk('path',{fill:'none',stroke:'#7c848f','stroke-width':'1.6'});
E.guard = mk('path',{fill:'#a9b0b9'});
E.grip = mk('path',{fill:'#6f4c30'});
E.pommel= mk('ellipse',{fill:'#a9b0b9'});
gSword.appendChild(E.blade);
gSword.appendChild(E.fuller);
gSword.appendChild(E.guard);
gSword.appendChild(E.grip);
gSword.appendChild(E.pommel);
svg.appendChild(gSword);
/* 14. right fist (over the grip) */
E.fistR = add(svg,'circle',{cx:302,cy:452,r:12,fill:'#c98f66'});
/* ============================================================
5. RENDER
============================================================ */
const slider = document.getElementById('slider');
const labelEl = document.getElementById('label');
function setGrad(g,a,b){ g[0].setAttribute('stop-color',a); g[1].setAttribute('stop-color',b); }
function drawSword(s){
const o = s.wristR;
const a = s.sword.tilt*Math.PI/180;
const ux = Math.sin(a), uy = -Math.cos(a);
const px = Math.cos(a), py = Math.sin(a);
const at = (t,n)=>[o[0]+ux*t+px*n, o[1]+uy*t+py*n];
const sw = s.sword;
const tg0 = -sw.gripLen/2, tg1 = sw.gripLen/2;
const tB0 = tg1 + sw.guardT;
const tTip = tB0 + sw.bladeLen;
E.grip.setAttribute('d', poly([at(tg0,6),at(tg1,6),at(tg1,-6),at(tg0,-6)]));
E.guard.setAttribute('d', poly([
at(tg1-sw.guardT/2, sw.guardW/2), at(tg1+sw.guardT/2, sw.guardW/2),
at(tg1+sw.guardT/2,-sw.guardW/2), at(tg1-sw.guardT/2,-sw.guardW/2)
]));
const w = sw.bladeW/2;
const tw = sw.bladeW*sw.tipFrac/2;
E.blade.setAttribute('d', poly([
at(tB0,w), at(tB0,-w), at(tTip-sw.tipLen,-tw), at(tTip,0), at(tTip-sw.tipLen,tw)
]));
const f0 = at(tB0+4,0), f1 = at(tTip-sw.tipLen*0.9,0);
E.fuller.setAttribute('d','M'+f0[0].toFixed(1)+','+f0[1].toFixed(1)+' L'+f1[0].toFixed(1)+','+f1[1].toFixed(1));
const pc = at(tg0 - sw.pommelRx*0.55, 0);
E.pommel.setAttribute('cx',pc[0].toFixed(1));
E.pommel.setAttribute('cy',pc[1].toFixed(1));
E.pommel.setAttribute('rx',sw.pommelRx);
E.pommel.setAttribute('ry',sw.pommelRy);
E.pommel.setAttribute('transform','rotate('+sw.tilt+' '+pc[0].toFixed(1)+' '+pc[1].toFixed(1)+')');
}
function render(){
const v = (+slider.value)/100;
const i = Math.min(1, Math.floor(v));
const u = v - i;
const s = mix(SPECS[i], SPECS[i+1], u);
/* gradients */
setGrad(gUnder, s.cUnder1, s.cUnder2);
setGrad(gArm, s.cArm1, s.cArm2);
setGrad(gSkirt, s.cSkirt1, s.cSkirt2);
setGrad(gLimb, s.cLimb1, s.cLimb2);
setGrad(gHelm, s.cHelm1, s.cHelm2);
setGrad(gShield,s.cShield1,s.cShield2);
/* torso */
const underD = smooth(s.torsoUnder);
const armD = smooth(s.torsoArm);
E.torsoUnder.setAttribute('d', underD);
E.mailTorso.setAttribute('d', underD);
E.mailTorso.setAttribute('opacity', s.oMail);
E.torsoArm.setAttribute('d', armD);
E.torsoArm.setAttribute('opacity', s.oArmour);
clipArmPath.setAttribute('d', armD);
E.lorica.style.opacity = s.oBands;
E.plate.style.opacity = s.oPlate;
/* skirt + fauld */
E.skirt.setAttribute('d', smooth(s.skirt));
E.fauld.style.opacity = s.oFauld;
/* belt */
E.belt.setAttribute('fill', s.cBelt);
/* legs / feet */
E.footL.setAttribute('d', poly(s.footL));
E.footR.setAttribute('d', poly(s.footR));
E.footL.setAttribute('fill', s.oLegArmour>0.5 ? s.cLimb1 : '#6b4a2f');
E.footR.setAttribute('fill', s.oLegArmour>0.5 ? s.cLimb1 : '#6b4a2f');
E.legLArm.setAttribute('opacity', s.oLegArmour);
E.legRArm.setAttribute('opacity', s.oLegArmour);
E.legLMail.setAttribute('opacity', s.oMail*s.oLegArmour);
E.legRMail.setAttribute('opacity', s.oMail*s.oLegArmour);
/* arms */
const aL = capsule(SH_L, s.elbowL, 34) + capsule(s.elbowL, s.wristL, 28);
const aR = capsule(SH_R, s.elbowR, 34) + capsule(s.elbowR, s.wristR, 28);
const bL = capsule(SH_L, s.elbowL, 42) + capsule(s.elbowL, s.wristL, 34);
const bR = capsule(SH_R, s.elbowR, 42) + capsule(s.elbowR, s.wristR, 34);
armL.body.setAttribute('d', aL);
armR.body.setAttribute('d', aR);
armL.arm.setAttribute('d', bL); armR.arm.setAttribute('d', bR);
armL.mail.setAttribute('d', bL); armR.mail.setAttribute('d', bR);
armL.arm.setAttribute('opacity', s.oArmArmour);
armR.arm.setAttribute('opacity', s.oArmArmour);
armL.mail.setAttribute('opacity', s.oMail*s.oArmArmour);
armR.mail.setAttribute('opacity', s.oMail*s.oArmArmour);
const midL = [(SH_L[0]+s.elbowL[0])/2,(SH_L[1]+s.elbowL[1])/2];
const midR = [(SH_R[0]+s.elbowR[0])/2,(SH_R[1]+s.elbowR[1])/2];
armL.sleeve.setAttribute('d', capsule(SH_L, midL, 38));
armR.sleeve.setAttribute('d', capsule(SH_R, midR, 38));
armL.sleeve.setAttribute('opacity', s.oSleeve);
armR.sleeve.setAttribute('opacity', s.oSleeve);
E.pauldronL.setAttribute('opacity', s.oPauldron);
E.pauldronR.setAttribute('opacity', s.oPauldron);
E.couterL.setAttribute('opacity', s.oCouter);
E.couterR.setAttribute('opacity', s.oCouter);
E.poleynL.setAttribute('opacity', s.oPoleyn);
E.poleynR.setAttribute('opacity', s.oPoleyn);
/* hands */
E.fistL.setAttribute('cx', s.wristL[0]);
E.fistL.setAttribute('cy', s.wristL[1]);
E.fistL.setAttribute('fill', s.cFist);
E.fistR.setAttribute('cx', s.wristR[0]);
E.fistR.setAttribute('cy', s.wristR[1]);
E.fistR.setAttribute('rx', 12);
E.fistR.setAttribute('r', 12);
E.fistR.setAttribute('fill', s.cFist);
/* head + helmet */
E.face.setAttribute('opacity', s.oFace);
E.helm.setAttribute('d', smooth(s.helm));
E.gRoman.style.opacity = s.oCheek;
E.gNorman.style.opacity = s.oNasal;
E.gMilan.style.opacity = s.oVisor;
/* shield */
const k = s.shieldScale;
E.gShield.setAttribute('transform',
'translate(128,690) scale('+k.toFixed(4)+') translate(-128,-690)');
E.gShield.setAttribute('opacity', k < 0.02 ? 0 : 1);
E.shieldFace.setAttribute('d', poly(s.shield));
E.shieldRim.setAttribute('d', poly(s.shield));
E.shieldBoss.setAttribute('cx', s.boss[0]);
E.shieldBoss.setAttribute('cy', s.boss[1]);
E.shieldBoss.setAttribute('r', s.bossR);
E.shieldBoss.setAttribute('opacity', s.oBoss);
E.shieldSpine.setAttribute('opacity', s.oSpine);
/* sword */
E.grip.setAttribute('fill', s.cGrip);
E.guard.setAttribute('fill', s.cGuard);
E.blade.setAttribute('fill', s.cBlade);
E.fuller.setAttribute('opacity', s.oFuller);
drawSword(s);
/* label */
renderLabel(v);
}
/* ============================================================
6. LABEL
============================================================ */
const TRANSITION = [
'The banded lorica is dissolving into a knee-length mail hauberk; the Imperial Gallic helmet is rising into a conical nasal helm; the curved scutum is stretching into a kite shield; and the gladius is lengthening into a knightly sword.',
'The mail hauberk is closing into articulated white plate; the kite shield is dwindling out of use; and the arming sword is lengthening with a narrower, more sharply tapered blade.'
];
function esc(t){ return t.replace(/&/g,'&').replace(/</g,'<'); }
function renderLabel(v){
const stop = (v===0)?0:(v===1)?1:(v===2)?2:-1;
if(stop >= 0){
const m = META[stop];
labelEl.innerHTML =
'<div class="lab-kicker">Exhibit '+String.fromCharCode(65+stop)+' · exact stop</div>' +
'<h2 class="lab-title">'+esc(m.title)+'</h2>' +
'<div class="lab-date">'+esc(m.date)+' — '+esc(m.place)+'</div>' +
'<div class="row"><b>Date</b><div>'+esc(m.date)+'</div></div>' +
'<div class="row"><b>Place</b><div>'+esc(m.place)+'</div></div>' +
'<div class="row"><b>Equipment</b><div>'+esc(m.equip)+'</div></div>' +
'<div class="row"><b>Materials</b><div>'+esc(m.mats)+'</div></div>' +
'<div class="row"><b>Weapon</b><div>'+esc(m.weapon)+'</div></div>' +
'<div class="note">Note — '+esc(m.note)+'</div>';
return;
}
const i = Math.min(1, Math.floor(v));
const u = v - i;
const A = META[i], B = META[i+1];
const pA = Math.round((1-u)*100), pB = Math.round(u*100);
labelEl.innerHTML =
'<div class="lab-kicker">In transition · between two stops</div>' +
'<h2 class="lab-title">Between '+esc(A.title)+' and '+esc(B.title)+'</h2>' +
'<div class="lab-date">This drawing is a blend of two periods — it belongs to neither.</div>' +
'<div class="blend"><i class="a" style="width:'+pA+'%"></i><i class="b" style="width:'+pB+'%"></i></div>' +
'<div class="blendcap"><span>'+pA+'% '+esc(A.short)+'</span><span>'+pB+'% '+esc(B.short)+'</span></div>' +
'<div class="row"><b>Date</b><div>'+esc(A.date)+' → '+esc(B.date)+'</div></div>' +
'<div class="row"><b>Place</b><div>'+esc(A.place)+' → '+esc(B.place)+'</div></div>' +
'<div class="row"><b>Equipment</b><div>'+esc(A.equip.split(';')[0])+' giving way to '+esc(B.equip.split(';')[0].toLowerCase())+'</div></div>' +
'<div class="row"><b>Materials</b><div>'+esc(A.mats)+' → '+esc(B.mats)+'</div></div>' +
'<div class="row"><b>Weapon</b><div>'+esc(A.weapon.split('—')[0])+' → '+esc(B.weapon.split('—')[0])+'</div></div>' +
'<div class="note">'+esc(TRANSITION[i])+'</div>';
}
/* ============================================================
7. WIRE UP
============================================================ */
slider.addEventListener('input', render);
render();
})();
</script>
</body>
</html>
The page keeps a fixed skeleton and swaps the attached gear by interpolating between period-specific shapes and colours.
Fixed body, movable arms
The head, torso, hips, knees, and ankles stay at their original coordinates. Only the arm joints move to new positions at Roman, Norman, and Milanese stops, so equipment can be held or worn without making the figure drift.
Layer order and visual blending
Inline SVG elements are stacked from shadow and legs up through torso armour, helmet, shield, and sword. For a limited set of parts, the script colour-mixes matching gradients and point arrays, then redraws paths; this keeps every intermediate sliding position readable and reversible.
Exact-stop vs. in-between labels
The slider value is mapped to a 0–200 range. When it lands exactly on 0, 100, or 200, a full label shows one period’s date, place, equipment, materials, weapon, and a note. In between, the panel names both adjacent periods, shows a blend percentage, and explains what is visually changing.
Weapon compatibility
Each period gets a same-era sword: a Pompeii-type gladius, an Oakeshott Type X knightly sword, and an Oakeshott Type XV arming sword. Blade length and proportion change smoothly along with the rest of the kit.
Optimization Tip: You can adjust the min, max, and step attributes on the #slider input if you want finer or coarser control over the blend. The exact stop values currently correspond to 0, 100, and 200; changing the range will also affect where the full period labels appear.