Items scanned0
Misplaced0
Missing0
StatusScanning
Illustration of scanning behaviour — not live data
/* ---------- palette + sizing (edit these) ---------- */
.stockbot{
--sb-height: 320px;
--sb-bg: #F6F5F1; /* aisle / page ground */
--sb-floor: #E7E4DC; /* floor band */
--sb-line: #DEDBD1; /* borders */
--sb-shelf: #BEBAAC; /* shelving steel */
--sb-shelf-2: #A9A497; /* shelf edge / uprights */
--sb-teal: #013E49; /* HKC petrol — robot detail, counted marks */
--sb-teal-soft: #7FA7B0;
--sb-gold: #D69D46; /* HKC gold — scan band, recognition boxes */
--sb-gold-deep: #8A6420;
--sb-ink: #0B2027;
--sb-ink-2: #4B5A61;
--sb-ink-3: #7D8B90;
--sb-surface: #FFFFFF; /* stats bar, robot body */
/* stock item tones */
--sb-i1: #C9B99B;
--sb-i2: #B4A488;
--sb-i3: #D8CDB6;
--sb-i4: #9FA9A4;
--sb-i5: #C2CBC9;
--sb-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--sb-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Courier New", monospace;
}
@media (prefers-color-scheme: dark){
.stockbot[data-scheme="auto"]{
--sb-bg: #06171C;
--sb-floor: #0B2229;
--sb-line: #17343C;
--sb-shelf: #2C4A52;
--sb-shelf-2: #3A5C65;
--sb-teal: #4FB8C9;
--sb-teal-soft: #2A6270;
--sb-gold: #E8B765;
--sb-gold-deep: #E8B765;
--sb-ink: #E7EFF0;
--sb-ink-2: #9DB2B7;
--sb-ink-3: #74898F;
--sb-surface: #0C232A;
--sb-i1: #7A6D55;
--sb-i2: #8B7C61;
--sb-i3: #665C48;
--sb-i4: #52605E;
--sb-i5: #45514F;
}
}
.stockbot[data-scheme="dark"]{
--sb-bg: #06171C;
--sb-floor: #0B2229;
--sb-line: #17343C;
--sb-shelf: #2C4A52;
--sb-shelf-2: #3A5C65;
--sb-teal: #4FB8C9;
--sb-teal-soft: #2A6270;
--sb-gold: #E8B765;
--sb-gold-deep: #E8B765;
--sb-ink: #E7EFF0;
--sb-ink-2: #9DB2B7;
--sb-ink-3: #74898F;
--sb-surface: #0C232A;
--sb-i1: #7A6D55;
--sb-i2: #8B7C61;
--sb-i3: #665C48;
--sb-i4: #52605E;
--sb-i5: #45514F;
}
/* ---------- structure ---------- */
.stockbot, .stockbot *{ box-sizing: border-box; }
.stockbot{
width: 100%;
font-family: var(--sb-font);
}
.stockbot__frame{
border: 1px solid var(--sb-line);
border-radius: 14px;
background: var(--sb-bg);
overflow: hidden;
}
.stockbot__canvas{
display: block;
width: 100%;
height: var(--sb-height);
max-width: 100%;
}
.stockbot__bar{
display: flex;
flex-wrap: wrap;
align-items: center;
border-top: 1px solid var(--sb-line);
background: var(--sb-surface);
}
.stockbot__stat{
display: flex;
align-items: baseline;
gap: 9px;
padding: 11px 20px;
border-right: 1px solid var(--sb-line);
font-family: var(--sb-mono);
font-size: 11px;
line-height: 1.4;
letter-spacing: .11em;
text-transform: uppercase;
color: var(--sb-ink-3);
}
.stockbot__stat b{
font-size: 14px;
font-weight: 600;
letter-spacing: .02em;
color: var(--sb-ink);
font-variant-numeric: tabular-nums;
}
.stockbot__stat--live b{ color: var(--sb-gold-deep); }
.stockbot__note{
margin-left: auto;
padding: 11px 20px;
font-family: var(--sb-mono);
font-size: 10px;
line-height: 1.4;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--sb-ink-3);
}
@media (max-width: 820px){
.stockbot__note{
margin-left: 0;
width: 100%;
border-top: 1px solid var(--sb-line);
}
.stockbot__stat{ padding: 10px 14px; }
}
(function () {
'use strict';
function initAll(){
var n = document.querySelectorAll('.stockbot');
for (var i = 0; i < n.length; i++) init(n[i]);
}
function init(root){
if (root.getAttribute('data-sb-ready') === '1') return;
root.setAttribute('data-sb-ready', '1');
var cv = root.querySelector('.stockbot__canvas');
if (!cv || !cv.getContext) return;
var ctx = cv.getContext('2d');
var elScanned = root.querySelector('[data-sb="scanned"]');
var elFlagged = root.querySelector('[data-sb="flagged"]');
var elMissing = root.querySelector('[data-sb="missing"]');
var elStatus = root.querySelector('[data-sb="status"]');
var motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
var W = 0, H = 0, dpr = 1;
var floorY = 0, shelfTop = 0, rows = 3, rowH = 0;
var items = [], uprights = [];
var bot = null;
var scanned = 0, flagged = 0, missing = 0;
var rafId = null, last = 0, running = false;
var C = {}, MODE = 'vision';
function readTokens(){
var s = getComputedStyle(root);
function g(n){ return s.getPropertyValue(n).trim(); }
C = {
bg:g('--sb-bg'), floor:g('--sb-floor'), line:g('--sb-line'),
shelf:g('--sb-shelf'), shelf2:g('--sb-shelf-2'),
teal:g('--sb-teal'), tealSoft:g('--sb-teal-soft'),
gold:g('--sb-gold'), goldDeep:g('--sb-gold-deep'),
ink:g('--sb-ink'), ink2:g('--sb-ink-2'), ink3:g('--sb-ink-3'),
surface:g('--sb-surface'),
items:[g('--sb-i1'),g('--sb-i2'),g('--sb-i3'),g('--sb-i4'),g('--sb-i5')],
mono:g('--sb-mono') || 'monospace'
};
MODE = (root.getAttribute('data-mode') || 'vision').toLowerCase();
if (MODE !== 'rfid') MODE = 'vision';
}
function rr(x,y,w,h,r){
r = Math.min(r, Math.abs(w)/2, Math.abs(h)/2);
ctx.beginPath();
ctx.moveTo(x+r,y);
ctx.arcTo(x+w,y,x+w,y+h,r);
ctx.arcTo(x+w,y+h,x,y+h,r);
ctx.arcTo(x,y+h,x,y,r);
ctx.arcTo(x,y,x+w,y,r);
ctx.closePath();
}
function build(){
floorY = H - Math.max(20, H * 0.08);
shelfTop = Math.max(14, H * 0.06);
rows = H < 240 ? 3 : 4;
rowH = (floorY - shelfTop) / rows;
// uprights every ~170px
uprights = [];
var step = Math.max(150, W / Math.max(2, Math.round(W / 190)));
for (var ux = 0; ux <= W + step; ux += step) uprights.push(ux);
items = [];
for (var r = 0; r < rows; r++){
var boardY = shelfTop + (r + 1) * rowH; // shelf board the row sits on
var x = 8;
while (x W - 8) break;
var gap = 2 + Math.random() * 3;
// ~7% of slots are empty — these become "missing" when scanned
var empty = Math.random() < 0.07;
var h = rowH * (0.52 + Math.random() * 0.34);
items.push({
x: x, w: w, h: h, row: r,
top: boardY - 3 - h,
col: C.items[(Math.random() * C.items.length) | 0],
empty: empty,
// ~6% of present items are misplaced
misplaced: (!empty && Math.random() scan -> done
t: 0
});
x += w + gap;
}
}
bot = {
x: -60,
speed: 52 + Math.random() * 10,
alpha: 0,
h: (floorY - shelfTop) * 0.92
};
}
function reset(){
build();
if (elStatus) elStatus.textContent = 'Scanning';
}
function scanX(){ return bot.x + 26; }
function update(dt){
bot.x += bot.speed * dt;
bot.alpha = Math.min(1, bot.alpha + dt * 2);
var sx = scanX();
for (var i = 0; i = it.x + it.w * 0.5){
it.state = 'scan'; it.t = 0;
if (it.empty){ missing++; }
else {
scanned++;
if (it.misplaced) flagged++;
}
} else if (it.state === 'scan'){
it.t += dt / 0.55;
if (it.t >= 1){ it.t = 1; it.state = 'done'; }
}
}
if (bot.x > W + 70){
scanned = 0; flagged = 0; missing = 0;
reset();
}
}
function drawShelving(){
// floor
ctx.fillStyle = C.floor;
ctx.fillRect(0, floorY, W, H - floorY);
ctx.strokeStyle = C.shelf2;
ctx.globalAlpha = .5;
ctx.beginPath(); ctx.moveTo(0, floorY + .5); ctx.lineTo(W, floorY + .5); ctx.stroke();
ctx.globalAlpha = 1;
// shelf boards
ctx.fillStyle = C.shelf;
for (var r = 0; r <= rows; r++){
var y = shelfTop + r * rowH;
ctx.fillRect(0, y, W, 3);
}
// uprights
ctx.fillStyle = C.shelf2;
for (var i = 0; i < uprights.length; i++){
ctx.fillRect(uprights[i], shelfTop, 4, floorY - shelfTop);
}
// bay labels
ctx.fillStyle = C.ink3;
ctx.font = '600 8px ' + C.mono;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
for (var j = 0; j < uprights.length - 1; j++){
ctx.fillText('A' + (j + 1), uprights[j] + 8, shelfTop + 5);
}
}
function drawItems(){
for (var i = 0; i < items.length; i++){
var it = items[i];
if (!it.empty){
ctx.fillStyle = it.col;
rr(it.x, it.top, it.w, it.h, 1.5);
ctx.fill();
// spine highlight
ctx.fillStyle = C.surface;
ctx.globalAlpha = .22;
ctx.fillRect(it.x + 1.5, it.top + 2, Math.max(1, it.w * 0.22), it.h - 4);
ctx.globalAlpha = 1;
}
// recognition annotation
if (it.state === 'scan'){
var e = 1 - Math.pow(1 - it.t, 2);
ctx.globalAlpha = 0.35 + 0.65 * (1 - it.t);
ctx.strokeStyle = it.empty ? C.ink3 : C.gold;
ctx.lineWidth = 1.4;
if (it.empty) ctx.setLineDash([3, 3]);
var pad = 2.5 * (1 - e) + 1.5;
rr(it.x - pad, it.top - pad, it.w + pad * 2, it.h + pad * 2, 2);
ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1;
}
if (it.state === 'done'){
if (it.empty){
// missing slot
ctx.strokeStyle = C.ink3;
ctx.globalAlpha = .55;
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
rr(it.x, it.top, it.w, it.h, 2);
ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1;
} else if (it.misplaced){
// flagged: persistent gold outline + marker dot
ctx.strokeStyle = C.gold;
ctx.lineWidth = 1.3;
rr(it.x - 1.5, it.top - 1.5, it.w + 3, it.h + 3, 2);
ctx.stroke();
ctx.fillStyle = C.gold;
ctx.beginPath();
ctx.arc(it.x + it.w / 2, it.top - 5, 2, 0, Math.PI * 2);
ctx.fill();
} else {
// counted: small teal tick under the item
ctx.fillStyle = C.teal;
ctx.globalAlpha = .8;
ctx.fillRect(it.x, it.top + it.h + 1.5, it.w, 1.4);
ctx.globalAlpha = 1;
}
}
}
}
function drawScan(){
var sx = scanX();
if (sx W + 30) return;
var bandW = MODE === 'rfid' ? 96 : 62;
var top = shelfTop, bot2 = floorY;
var grad = ctx.createLinearGradient(sx - bandW / 2, 0, sx + bandW / 2, 0);
grad.addColorStop(0, 'rgba(0,0,0,0)');
grad.addColorStop(0.5, C.gold);
grad.addColorStop(1, 'rgba(0,0,0,0)');
ctx.globalAlpha = MODE === 'rfid' ? .13 : .18;
ctx.fillStyle = grad;
ctx.fillRect(sx - bandW / 2, top, bandW, bot2 - top);
ctx.globalAlpha = 1;
// leading edge
ctx.strokeStyle = C.gold;
ctx.globalAlpha = .75;
ctx.lineWidth = 1.2;
ctx.beginPath(); ctx.moveTo(sx, top); ctx.lineTo(sx, bot2); ctx.stroke();
ctx.globalAlpha = 1;
}
function drawBot(){
var baseW = 34, baseH = 13;
var x = bot.x;
var y = floorY;
var topY = y - bot.h;
ctx.save();
ctx.globalAlpha = bot.alpha;
// ground glow
ctx.fillStyle = C.teal;
ctx.globalAlpha = bot.alpha * .12;
ctx.beginPath();
ctx.ellipse(x, y + 2, baseW * 0.85, 5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = bot.alpha;
// sensing rays from the head toward the shelves
var headY = topY + 10;
ctx.strokeStyle = MODE === 'rfid' ? C.gold : C.teal;
ctx.globalAlpha = bot.alpha * .45;
ctx.lineWidth = 1;
if (MODE === 'rfid'){
for (var k = 1; k <= 3; k++){
ctx.beginPath();
ctx.arc(x + 8, y - bot.h * 0.5, 12 + k * 11, -Math.PI * 0.42, Math.PI * 0.42);
ctx.stroke();
}
} else {
ctx.beginPath();
ctx.moveTo(x + 6, headY); ctx.lineTo(scanX() + 22, shelfTop + 6);
ctx.moveTo(x + 6, headY); ctx.lineTo(scanX() + 22, floorY - 6);
ctx.stroke();
}
ctx.globalAlpha = bot.alpha;
// mast
ctx.strokeStyle = C.ink2;
ctx.lineWidth = 4;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x, topY + 26); ctx.lineTo(x, y - baseH - 26);
ctx.stroke();
// camera head
ctx.fillStyle = C.ink;
rr(x - 5, topY, 10, 18, 2.5);
ctx.fill();
ctx.fillStyle = MODE === 'rfid' ? C.gold : C.teal;
ctx.beginPath(); ctx.arc(x, topY + 9, 2.4, 0, Math.PI * 2); ctx.fill();
// gold collar
ctx.fillStyle = C.surface;
ctx.strokeStyle = C.gold;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(x, topY + 34, 15, 8, 0, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
// tapered body
var bodyTop = topY + 40;
var bodyBot = y - baseH - 1;
ctx.beginPath();
ctx.moveTo(x - 8, bodyTop);
ctx.quadraticCurveTo(x - 10, (bodyTop + bodyBot) / 2, x - 19, bodyBot);
ctx.lineTo(x + 19, bodyBot);
ctx.quadraticCurveTo(x + 10, (bodyTop + bodyBot) / 2, x + 8, bodyTop);
ctx.closePath();
ctx.fillStyle = C.surface;
ctx.fill();
ctx.strokeStyle = C.shelf2;
ctx.lineWidth = 1;
ctx.stroke();
// base + gold ring
ctx.fillStyle = C.surface;
rr(x - baseW / 2, y - baseH, baseW, baseH, 5);
ctx.fill();
ctx.strokeStyle = C.gold;
ctx.lineWidth = 2;
rr(x - baseW / 2, y - baseH, baseW, baseH, 5);
ctx.stroke();
ctx.fillStyle = C.teal;
ctx.fillRect(x - 7, y - baseH / 2 - 1, 14, 2);
ctx.restore();
}
function draw(){
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = C.bg;
ctx.fillRect(0, 0, W, H);
drawShelving();
drawItems();
drawScan();
drawBot();
}
function syncStats(){
if (elScanned && elScanned.textContent !== String(scanned)) elScanned.textContent = scanned.toLocaleString('en-US');
if (elFlagged && elFlagged.textContent !== String(flagged)) elFlagged.textContent = String(flagged);
if (elMissing && elMissing.textContent !== String(missing)) elMissing.textContent = String(missing);
}
function frame(now){
if (!running) return;
var dt = Math.min(0.05, (now - last) / 1000 || 0);
last = now;
update(dt);
draw();
syncStats();
rafId = requestAnimationFrame(frame);
}
function start(){
if (running || motionQuery.matches || !W) return;
running = true;
last = performance.now();
rafId = requestAnimationFrame(frame);
}
function stop(){
running = false;
if (rafId) cancelAnimationFrame(rafId);
rafId = null;
}
function staticFrame(){
// pose a representative still: robot mid-run, shelves part-scanned
bot.x = W * 0.46;
bot.alpha = 1;
var sx = scanX();
scanned = 0; flagged = 0; missing = 0;
for (var i = 0; i < items.length; i++){
var it = items[i];
if (it.x + it.w * 0.5 < sx){
it.state = 'done'; it.t = 1;
if (it.empty) missing++; else { scanned++; if (it.misplaced) flagged++; }
}
}
draw();
syncStats();
if (elStatus) elStatus.textContent = 'Scanning';
}
function resize(){
var r = cv.getBoundingClientRect();
if (!r.width) return;
dpr = Math.min(window.devicePixelRatio || 1, 2);
cv.width = Math.round(r.width * dpr);
cv.height = Math.round(r.height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
W = r.width; H = r.height;
readTokens();
scanned = 0; flagged = 0; missing = 0;
build();
if (motionQuery.matches) staticFrame(); else draw();
}
var rt;
window.addEventListener('resize', function(){
clearTimeout(rt);
rt = setTimeout(resize, 140);
});
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
function repaint(){ readTokens(); if (!running) draw(); }
if (darkQuery.addEventListener) darkQuery.addEventListener('change', repaint);
if (window.MutationObserver){
new MutationObserver(function(){ readTokens(); if (!running) draw(); })
.observe(root, { attributes: true, attributeFilter: ['data-scheme', 'data-mode'] });
}
if (motionQuery.addEventListener){
motionQuery.addEventListener('change', function(){
if (motionQuery.matches){ stop(); staticFrame(); } else { start(); }
});
}
document.addEventListener('visibilitychange', function(){
if (document.hidden) stop(); else start();
});
if ('IntersectionObserver' in window){
new IntersectionObserver(function(entries){
if (entries[0].isIntersecting) start(); else stop();
}, { threshold: 0.02 }).observe(cv);
}
function boot(){ resize(); if (!motionQuery.matches) start(); }
if (document.fonts && document.fonts.ready) document.fonts.ready.then(boot).catch(boot);
else boot();
}
if (document.readyState === 'loading'){
document.addEventListener('DOMContentLoaded', initAll);
} else {
initAll();
}
})();