It's good enough alright!
Here, ask it the rest:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Toyobi Field Animator — Direction · Intensity · Phase</title>
<style>
:root{
--bg:#0b1020; --panel:#0f1724; --muted:#9aa4bf; --accent:#7ee3c7; --glass: rgba(255,255,255,0.04);
}
html,body{height:100%;margin:0;font-family:Inter,ui-sans-serif,system-ui,Arial;color:#e6eef8;background:linear-gradient(180deg,#071025 0%,#081220 60%);}
.app{display:grid;grid-template-columns:360px 1fr;gap:18px;height:100vh;padding:18px;box-sizing:border-box}
.panel{background:var(--panel);border-radius:12px;padding:14px;box-shadow:0 6px 24px rgba(2,6,23,0.6);overflow:auto}
h1{font-size:16px;margin:0 0 10px;color:var(--accent)}
label{display:block;font-size:12px;color:var(--muted);margin-top:10px}
select,input[type=range],button{width:100%;margin-top:6px;background:var(--glass);border:1px solid rgba(255,255,255,0.04);color:#dbe9f8;padding:8px;border-radius:8px}
.canvas-wrap{position:relative;border-radius:12px;overflow:hidden}
#fieldCanvas{width:100%;height:100%;display:block;background:linear-gradient(180deg, rgba(10,14,22,0.6), rgba(2,4,10,0.2));}
.controls-row{display:flex;gap:8px;margin-top:10px}
.mini{flex:1}
.legend{font-size:12px;color:var(--muted);margin-top:12px}
.footer{font-size:12px;color:#8ea1c2;margin-top:12px}
.stat{font-size:13px;color:#cfeee0;margin-top:8px}
.badge{display:inline-block;padding:6px 8px;border-radius:8px;background:rgba(255,255,255,0.02);margin-right:6px}
.top-row{display:flex;justify-content:space-between;align-items:center}
.linkbtn{background:transparent;border:1px solid rgba(255,255,255,0.04);padding:6px 8px;border-radius:8px;color:var(--muted)}
@media (max-width:900px){.app{grid-template-columns:1fr;grid-auto-rows:420px}}
</style>
</head>
<body>
<div class="app">
<div class="panel">
<div class="top-row">
<h1>Toyobi Field Animator</h1>
<button id="resetBtn" class="linkbtn">Reset</button>
</div>
<label for="stone">Stone</label>
<select id="stone"></select>
<label for="chemical">Chemical</label>
<select id="chemical"></select>
<label for="intensity">Global Intensity — scale</label>
<input id="intensity" type="range" min="0" max="200" value="80">[Expand Post]
<label for="frequency">Wave Frequency — Hz (visual)</label>
<input id="frequency" type="range" min="0.1" max="6" step="0.1" value="1.2">
<label for="phaseShift">Phase Shift — degrees</label>
<input id="phaseShift" type="range" min="0" max="360" value="0">
<div class="controls-row">
<button id="playBtn">Pause</button>
<button id="exportBtn">Export JSON</button>
</div>
<div class="legend">
<div><span class="badge">Direction</span> crystal axis (vector)</div>
<div><span class="badge">Intensity</span> vector length & opacity</div>
<div><span class="badge">Phase</span> hue rotation and wave offset</div>
</div>
<div class="stat" id="statLine">Selected: —</div>
<div class="footer">Click canvas to place a local probe. Hover vectors to see values.</div>
</div>
<div class="canvas-wrap panel" style="padding:0">
<canvas id="fieldCanvas"></canvas>
</div>
</div>
<script>
// --------- Sample database (editable) ---------
const STONES = {
quartz: {name:'Quartz', axis:[0, -1], baseIntensity:1.0, notes:'piezoelectric; c-axis focal'},
tourmaline: {name:'Tourmaline', axis:[0,-1], baseIntensity:0.9, notes:'strong polarization along long axis'},
calcite: {name:'Calcite', axis:[1,0], baseIntensity:0.6, notes:'birefringent double-axis behavior'},
hematite: {name:'Hematite', axis:[-1,0], baseIntensity:1.2, notes:'magnetic domain alignment'},
malachite: {name:'Malachite', axis:[0.6,-0.8], baseIntensity:0.7, notes:'catalytic copper surface interactions'}
};
const CHEMICALS = {
water: {name:'Water', perm:78, reactive:0.2, phaseMod:1.0},
alcohol: {name:'Alcohol', perm:25, reactive:0.15, phaseMod:0.9},
HCl: {name:'Hydrochloric Acid', perm:60, reactive:0.9, phaseMod:0.6},
NaOH: {name:'Sodium Hydroxide', perm:50, reactive:0.85, phaseMod:0.7},
NH3: {name:'Ammonia', perm:16, reactive:0.4, phaseMod:1.1}
};
// --------- Utilities ---------
function $(id){return document.getElementById(id)}
function lerp(a,b,t){return a+(b-a)*t}
function clamp(v,a,b){return Math.max(a,Math.min(b,v))}
// --------- UI population ---------
const stoneSel = $('stone'); const chemSel = $('chemical');
for(const k of Object.keys(STONES)){
const o = document.createElement('option'); o.value=k; o.textContent = STONES[k].name; stoneSel.appendChild(o);
}
for(const k of Object.keys(CHEMICALS)){
const o = document.createElement('option'); o.value=k; o.textContent = CHEMICALS[k].name; chemSel.appendChild(o);
}
// --------- Canvas setup ---------
const canvas = $('fieldCanvas'); const ctx = canvas.getContext('2d');
let W=0,H=0,paused=false, time=0;
function resize(){W=canvas.width=canvas.clientWidth;H=canvas.height=canvas.clientHeight;}
window.addEventListener('resize',resize); resize();
// Local probes placed by clicks
const probes = [];
canvas.addEventListener('click',(e)=>{
const r = canvas.getBoundingClientRect(); probes.push({x:e.clientX-r.left,y:e.clientY-r.top});
});
// --------- Field model ---------
function fieldAt(x,y,stoneKey,chemKey,t){
// Normalize coordinates to -1..1
const nx = (x/W)*2-1; const ny = (y/H)*2-1;
const stone = STONES[stoneKey]; const chem = CHEMICALS[chemKey];
// Direction vector (unit) from stone axis
let dir = stone.axis.slice(); const mag = Math.hypot(dir[0],dir[1])||1; dir = [dir[0]/mag, dir[1]/mag];
// Intensity: base * global slider * distance falloff * chemical reactive modifier
const globalI = Number($('intensity').value)/80; // normalized
const dist = Math.hypot(nx-dir[0]*0.2, ny-dir[1]*0.2);
const falloff = 1/(1+Math.pow(dist*3,2));
const intensity = stone.baseIntensity * chem.reactive * globalI * falloff;
// Phase: base wave from frequency, modulated by chemical phaseMod and user phaseShift
const freq = Number($('frequency').value);
const userPhase = Number($('phaseShift').value) * Math.PI/180;
const phase = Math.sin( ( (nx*2 + ny*2) * freq * 0.4 ) + t*freq*0.7 + userPhase ) * chem.phaseMod;
// Per-vector angle: base axis angle plus small local rotation from phase
const baseAngle = Math.atan2(dir[1],dir[0]);
const angle = baseAngle + 0.6*phase;
return {vx:Math.cos(angle), vy:Math.sin(angle), intensity:clamp(intensity,0,2), phase, angle};
}
// --------- Draw primitives ---------
function drawVector(x,y,vx,vy,intensity,phase){
const len = 40 * intensity; const alpha = clamp(intensity*0.9,0.08,0.98);
const hue = (180 + phase*80) % 360; // phase → hue shift
ctx.save();
ctx.translate(x,y);
ctx.rotate(Math.atan2(vy,vx));
// tail
ctx.globalAlpha = alpha*0.9;
ctx.lineWidth = 3; ctx.lineCap='round';
ctx.strokeStyle = `hsla(${hue},80%,60%,${alpha})`;
ctx.beginPath(); ctx.moveTo(-len*0.2,0); ctx.lineTo(len,0); ctx.stroke();
// head
ctx.beginPath(); ctx.moveTo(len,0); ctx.lineTo(len-10,-6); ctx.lineTo(len-10,6); ctx.closePath();
ctx.fillStyle = `hsla(${hue},90%,60%,${alpha})`; ctx.fill();
// glow
ctx.beginPath(); ctx.arc(len*0.2,0,6+intensity*6,0,Math.PI*2); ctx.fillStyle = `rgba(200,240,210,${alpha*0.06})`; ctx.fill();
ctx.restore();
}
function drawWave(x,y,t,intensity,phase){
const amp = 10*intensity*(0.6+phase*0.4);
ctx.beginPath();
for(let i=0;i<120;i++){
const px = x - 60 + (i/119)*120;
const py = y + Math.sin((i*0.2) + t*1.8 + phase*2) * amp;
if(i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
}
ctx.lineWidth = 1.6; ctx.strokeStyle = `rgba(200,230,220,${0.06+intensity*0.12})`; ctx.stroke();
}
// --------- Animation loop ---------
function frame(ts){
if(!lastTS) lastTS = ts; const dt = (ts-lastTS)/1000; lastTS = ts; if(!paused) time += dt;
ctx.clearRect(0,0,W,H);
// subtle background grid
ctx.save();
ctx.fillStyle='rgba(255,255,255,0.02)'; ctx.fillRect(0,0,W,H);
ctx.restore();
const stoneKey = stoneSel.value; const chemKey = chemSel.value;
// draw vector field grid
const nx = 9, ny = 7; for(let iy=0;iy<ny;iy++){
for(let ix=0;ix<nx;ix++){
const x = (ix+0.5)/nx*W; const y = (iy+0.5)/ny*H;
const f = fieldAt(x,y,stoneKey,chemKey,time);
drawVector(x,y,f.vx,f.vy,f.intensity,f.phase);
// subtle local wave near center area
if(ix=Math.floor(nx/2) && iy=Math.floor(ny/2)) drawWave(x,y+40,time,f.intensity,f.phase);
}
}
// Draw probes
for(const p of probes){
const f = fieldAt(p.x,p.y,stoneKey,chemKey,time);
drawVector(p.x,p.y,f.vx,f.vy,f.intensity,f.phase);
ctx.fillStyle='rgba(255,255,255,0.02)'; ctx.beginPath(); ctx.arc(p.x,p.y,26,0,Math.PI*2); ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.04)'; ctx.fillRect(p.x-40,p.y+32,80,40);
ctx.fillStyle='rgba(200,240,210,0.95)'; ctx.font='12px monospace'; ctx.fillText(`I:${f.intensity.toFixed(2)} P:${f.phase.toFixed(2)}`,p.x-36,p.y+52);
}
// UI Stat update
const stone = STONES[stoneKey]; const chem = CHEMICALS[chemKey];
$('statLine').textContent = `Selected: ${stone.name} ↔ ${chem.name} · baseI=${stone.baseIntensity.toFixed(2)} · perm=${chem.perm}`;
requestAnimationFrame(frame);
}
let lastTS=0; requestAnimationFrame(frame);
// --------- Controls ---------
$('playBtn').addEventListener('click',()=>{ paused=!paused; $('playBtn').textContent = paused? 'Play':'Pause'; });
$('resetBtn').addEventListener('click',()=>{ probes.length=0; $('intensity').value=80; $('frequency').value=1.2; $('phaseShift').value=0; });
$('exportBtn').addEventListener('click',()=>{
const data = {stone:stoneSel.value, chemical:chemSel.value, intensity: $('intensity').value, frequency:$('frequency').value, phaseShift:$('phaseShift').value, probes};
const blob = new Blob([JSON.stringify(data,null,2)],{type:'application/json'});
const url = URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='toyobi-field.json'; a.click(); URL.revokeObjectURL(url);
});
// Initialize defaults
stoneSel.value='quartz'; chemSel.value='water';
// ensure canvas fills its container on load
function fitCanvas(){ canvas.style.width = '100%'; canvas.style.height = '100%'; resize(); }
fitCanvas();
// small accessibility: keyboard to toggle play/pause
window.addEventListener('keydown',(e)=>{ if(e.key===' ') { e.preventDefault(); $('playBtn').click(); } });
// Tooltip for hover - show vector values
canvas.addEventListener('mousemove',(e)=>{
const r = canvas.getBoundingClientRect(); const x=e.clientX-r.left, y=e.clientY-r.top;
// no heavy compute: sample nearby grid point
const f = fieldAt(x,y,stoneSel.value,chemSel.value,time);
canvas.title = `angle=${(f.angle*180/Math.PI).toFixed(0)}° · intensity=${f.intensity.toFixed(2)} · phase=${f.phase.toFixed(2)}`;
});
</script>
</body>
</html>
Same thing index.html and browser