<!DOCTYPE html><html><head><meta charset="utf-8" /><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'unsafe-inline'; img-src https: data:; font-src https: data:; media-src https: data:; connect-src https://cdn.jsdelivr.net; frame-src 'none'" /><script>
(function(){
var nav=navigator;
var reduced=matchMedia("(prefers-reduced-motion: reduce)").matches;
var conn=nav.connection||{};
var saveData=!!conn.saveData;
var slow=/(^|[^a-z])(2g|slow-2g)$/.test(conn.effectiveType||"");
var mem=nav.deviceMemory||8;
var cores=nav.hardwareConcurrency||8;
var webgl2=false;
try{var c=document.createElement("canvas");webgl2=!!(c.getContext("webgl2")||c.getContext("webgl"));}catch(e){}
var tier=1;
if(reduced||saveData||slow)tier=4;
else if(mem<=2||cores<=2||!webgl2)tier=3;
else if(mem<=4||cores<=4||conn.effectiveType==="3g")tier=2;
var q=new URLSearchParams(location.search||"");
var forced=parseInt(document.documentElement.getAttribute("data-te-tier")||q.get("tier")||"",10);
if(forced>=1&&forced<=4)tier=forced;
var listeners={tier:[],scroll:[],visible:[],copy:[]};
var copy=null;
document.documentElement.style.setProperty("--te-band","0.5");
var visible=true,hidden=document.hidden;
var loops=[];var loopHandle=0;var last=0;
var nativeRAF=window.requestAnimationFrame.bind(window);
var pending=[];
function active(){return visible&&!hidden&&tier<4}
function pump(now){
loopHandle=0;
if(!active())return;
var dt=last?Math.min(64,now-last):16;last=now;
for(var i=0;i<loops.length;i++){try{loops[i](dt,now)}catch(e){if(!loops[i].te_err){loops[i].te_err=1;console.error("TE.loop",e)}}}
var queued=pending;pending=[];
for(var j=0;j<queued.length;j++){try{queued[j](now)}catch(e){if(!queued[j].te_err){queued[j].te_err=1;console.error("TE.raf",e)}}}
// A callback above may already have scheduled the next frame through the
// wrapped requestAnimationFrame; never queue pump twice for one frame.
if(!loopHandle&&(loops.length||pending.length))loopHandle=nativeRAF(pump);
}
function schedule(){if(!loopHandle&&active())loopHandle=nativeRAF(pump)}
window.requestAnimationFrame=function(fn){pending.push(fn);schedule();return pending.length};
window.cancelAnimationFrame=function(){};
var videoState=new Map();
function pauseAll(){
document.querySelectorAll("video").forEach(function(v){videoState.set(v,!v.paused);if(!v.paused)v.pause()});
}
function resumeAll(){
document.querySelectorAll("video").forEach(function(v){if(videoState.get(v)){var p=v.play();if(p&&p.catch)p.catch(function(){})}});
last=0;schedule();
}
function setVisible(v){
if(v===visible)return;visible=v;
if(v)resumeAll();else pauseAll();
listeners.visible.forEach(function(f){f(v)});
}
document.addEventListener("visibilitychange",function(){hidden=document.hidden;if(hidden)pauseAll();else resumeAll()});
var frames=0,slowSince=0,checked=0;
loops.push(function(dt,now){
frames++;
if(now-checked<1000)return;
var fps=frames*1000/(now-checked);frames=0;checked=now;
if(fps<40){if(!slowSince)slowSince=now;else if(now-slowSince>2000){degrade();slowSince=0}}
else slowSince=0;
});
function degrade(){
if(tier>=3)return;tier++;
document.documentElement.setAttribute("data-te-tier",String(tier));
listeners.tier.forEach(function(f){f(tier)});
}
var pointer={x:0.5,y:0.5,vx:0,vy:0,down:false,fine:matchMedia("(pointer:fine)").matches,active:false};
var px=0,py=0,pt=0;
function onMove(e){
var w=innerWidth||1,h=innerHeight||1,now=performance.now();
var x=e.clientX/w,y=e.clientY/h;
if(pt){var s=Math.max(1,now-pt);pointer.vx=(x-px)*1000/s;pointer.vy=(y-py)*1000/s}
px=x;py=y;pt=now;pointer.x=x;pointer.y=y;pointer.active=true;
}
addEventListener("pointermove",onMove,{passive:true});
addEventListener("pointerdown",function(e){pointer.down=true;onMove(e)},{passive:true});
addEventListener("pointerup",function(){pointer.down=false},{passive:true});
addEventListener("pointerleave",function(){pointer.active=false;pointer.vx=0;pointer.vy=0});
addEventListener("message",function(e){
var d=e.data;if(!d||typeof d!=="object")return;
if(d.type==="te:scroll"&&typeof d.t==="number")listeners.scroll.forEach(function(f){f(d.t)});
if(d.type==="te:visible")setVisible(!!d.visible);
if(d.type==="te:copy"&&typeof d.y0==="number"){copy={x0:d.x0,y0:d.y0,x1:d.x1,y1:d.y1};document.documentElement.style.setProperty("--te-band",String(Math.round(d.y0*1000)/1000));listeners.copy.forEach(function(f){f(copy)})}
if(d.type==="te:tier"&&d.tier>=1&&d.tier<=4){tier=d.tier;document.documentElement.setAttribute("data-te-tier",String(tier));listeners.tier.forEach(function(f){f(tier)})}
});
document.documentElement.setAttribute("data-te-tier",String(tier));
window.TE={
get tier(){return tier},
get visible(){return visible&&!hidden},
get reduced(){return reduced},
get dpr(){return tier===1?Math.min(devicePixelRatio||1,2):tier===2?Math.min(devicePixelRatio||1,1.5):1},
caps:{memory:mem,cores:cores,webgl:webgl2,saveData:saveData,effectiveType:conn.effectiveType||"",reduced:reduced},
pointer:pointer,
loop:function(fn){loops.push(fn);schedule();return function(){var i=loops.indexOf(fn);if(i>=0)loops.splice(i,1)}},
onScroll:function(fn){listeners.scroll.push(fn)},
onTier:function(fn){listeners.tier.push(fn)},
onVisible:function(fn){listeners.visible.push(fn)},
onCopy:function(fn){listeners.copy.push(fn);if(copy)fn(copy)},
get copy(){return copy},
get band(){return copy?copy.y0:0.5},
degrade:degrade,
video:function(el,src){
if(tier>=4)return false;
var small=tier>=2||Math.min(innerWidth,innerHeight)<700;
el.src=small?src.replace(/\.mp4$/,"-sm.mp4"):src;
if(tier>=3)el.preload="metadata";
return true;
},
rendition:function(){return tier>=2||Math.min(innerWidth,innerHeight)<700?"-sm":""},
debug:function(){return {handle:loopHandle,loops:loops.length,pending:pending.length,visible:visible,hidden:hidden,tier:tier,active:active()}}
};
})();
</script><style>html,body{margin:0;padding:0;overflow:hidden;background:transparent}</style><script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script></head><body><style>
html,body{margin:0;height:100%;background:#f7f1e7;overflow:hidden;font-family:'Manrope',system-ui,sans-serif;color:#2a1d14}
#c,#g{position:absolute;inset:0;width:100%;height:100%;display:block}
#g{display:none}
#lvl{position:absolute;left:24px;top:16px;font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:rgba(42,29,20,.55);line-height:1.9}
#lvl b{color:#a2593b;font-weight:400}
#tmp{position:absolute;right:24px;top:16px;text-align:right;font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:rgba(42,29,20,.42);line-height:1.9}
#hint{position:absolute;left:0;right:0;top:calc(var(--te-band,.5)*100% - 24px);text-align:center;font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.24em;text-transform:uppercase;color:rgba(42,29,20,.4);transition:opacity .6s}
@media (max-width:640px){#lvl,#tmp{font-size:9px;letter-spacing:.12em}#lvl{left:14px}#tmp{right:14px}#hint{font-size:9px;letter-spacing:.16em}}
</style>
<canvas id=c></canvas><canvas id=g></canvas><div id=lvl></div><div id=tmp></div>
<p id=hint>Gulir, sangrainya berjalan</p>
<script>
window.TE3=function(o){
var TE=window.TE;
if(!TE||!TE.caps||!TE.caps.webgl)return false;
if(TE.tier>(o.tier||2))return false;
if(typeof THREE==="undefined")return false;
var gc=document.getElementById(o.canvas||"g");
if(!gc)return false;
var ren;
try{ren=new THREE.WebGLRenderer({canvas:gc,antialias:o.aa!==false,alpha:!!o.alpha})}catch(e){return false}
if(THREE.SRGBColorSpace)ren.outputColorSpace=THREE.SRGBColorSpace;
if(o.tone){ren.toneMapping=THREE.ACESFilmicToneMapping;ren.toneMappingExposure=o.tone}
if(!o.alpha)ren.setClearColor(o.clear===undefined?0x000000:o.clear,1);
var scene=new THREE.Scene();
var cam=new THREE.PerspectiveCamera(o.fov||32,1,0.05,160);
cam.position.set(o.camX||0,o.camY||0,o.dist||4);
cam.lookAt(o.lookX||0,o.lookY||0,0);
var root=new THREE.Group();scene.add(root);
var k={THREE:THREE,ren:ren,scene:scene,cam:cam,root:root,band:0.5,w:0,h:0,
narrow:false,px:0,py:0,pw:0,t:0,visW:0,visH:0,tier:TE.tier,dpr:1,time:0};
// A scene that throws while it is being built must not take the stage down
// with it: the 2D canvas is still there and still correct, so we hand back
// false and let it boot. Same for the frame loop, which freezes on the last
// good frame rather than throwing sixty times a second.
try{if(o.build&&o.build(k)===false)return false}catch(e){return false}
var dead=o.replace===null?null:document.getElementById(o.replace||"c");
if(dead&&dead.parentNode)dead.parentNode.removeChild(dead);
gc.style.display="block";
function layout(){
k.w=innerWidth;k.h=innerHeight;k.narrow=k.w<(o.narrow||640);
if(o.host){var hb=o.host.getBoundingClientRect();if(hb.width>1&&hb.height>1){k.w=hb.width;k.h=hb.height}}
// A hero shares its frame with the headline, so the subject is fitted into
// the band the parent reports. An embed owns its whole box, so it passes
// band:1 and gets all of it.
k.band=o.band!==undefined?o.band:Math.max(TE.band||0.5,o.minBand||0.24);
k.dpr=Math.min(TE.dpr,o.maxDpr||1.75);
ren.setPixelRatio(k.dpr);ren.setSize(k.w,k.h,false);
cam.aspect=k.w/Math.max(1,k.h);cam.updateProjectionMatrix();
k.visH=2*Math.tan(cam.fov*Math.PI/360)*cam.position.z;
k.visW=k.visH*cam.aspect;
if(o.fit!==false){
var size=o.size||1;
var s=Math.min(k.band*k.visH*(o.fitH||0.74)/size,k.visW*(o.fitW||0.84)/size);
root.scale.setScalar(Math.max(o.minScale||0.12,s));
root.position.y=(0.5-k.band*0.5)*k.visH+(o.lift||0)*root.scale.x;
}
try{if(o.layout)o.layout(k)}catch(e){}
}
layout();addEventListener("resize",layout);
if(TE.onCopy)TE.onCopy(layout);
var frozen=false;
if(TE.onTier)TE.onTier(function(n){k.tier=n;if(n>=3)frozen=true});
addEventListener("pointermove",function(e){
k.px=(e.clientX/Math.max(1,innerWidth))*2-1;
k.py=-((e.clientY/Math.max(1,innerHeight))*2-1);
k.pw=1;
try{if(o.onPointer)o.onPointer(k)}catch(e){}
},{passive:true});
if(o.onScroll){
var os=function(v){k.t=Math.min(1,Math.max(0,v));try{o.onScroll(k)}catch(e){}};
if(TE.onScroll)TE.onScroll(os);
else addEventListener("message",function(e){if(e.data&&e.data.type==="te:scroll")os(e.data.t)});
}
TE.loop(function(dt){
if(frozen)return;
k.time+=dt;
k.pw*=Math.pow(0.988,dt/16);
try{
if(o.frame)o.frame(dt,k);
ren.render(scene,cam);
}catch(e){frozen=true}
});
return true;
};
</script>
<script>
(function(){
var TE=window.TE,cv=document.getElementById("c"),cx=cv.getContext("2d"),dpr=TE?Math.min(TE.dpr,2):1;
var lvlEl=document.getElementById("lvl"),tmpEl=document.getElementById("tmp"),hint=document.getElementById("hint");
var W=0,H=0,t=0,hid=false,COLS=0,ROWS=0,B=[];
var FC=0.38;
// the real roast continuum, one stop per named level
var STOP=[
[0.00,121,133,90,"Hijau",95,24,0],
[0.16,198,167,101,"Kuning",84,152,252],
[0.38,176,122,69,"Retak pertama",74,196,528],
[0.58,143,84,48,"Sangrai terang",62,202,612],
[0.80,99,54,30,"Sangrai sedang",46,208,684],
[1.00,59,32,20,"Sangrai gelap",25,217,768]];
var sd=97;function rnd(){sd=(sd*9301+49297)%233280;return sd/233280}
function lerp(a,b,k){return a+(b-a)*k}
function at(p){
if(p<0)p=0;if(p>1)p=1;
var i=0;while(i<STOP.length-2&&p>STOP[i+1][0])i++;
var A=STOP[i],Z=STOP[i+1],k=(p-A[0])/(Z[0]-A[0]);if(k<0)k=0;if(k>1)k=1;
return {r:lerp(A[1],Z[1],k),g:lerp(A[2],Z[2],k),b:lerp(A[3],Z[3],k),
n:k<0.5?A[4]:Z[4],a:lerp(A[5],Z[5],k),T:lerp(A[6],Z[6],k),s:lerp(A[7],Z[7],k)};
}
function cl(v){return v<0?0:v>255?255:Math.round(v)}
function rgb(o,m,j){var k=j||[0,0,0];return "rgb("+cl(o.r*m+k[0])+","+cl(o.g*m+k[1])+","+cl(o.b*m+k[2])+")"}
function mmss(s){var m=Math.floor(s/60),x=Math.round(s%60);return m+":"+(x<10?"0":"")+x}
function build(){
var narrow=W<640,tier=TE?TE.tier:1;
COLS=narrow?9:(tier>=3?16:20);ROWS=narrow?8:6;
sd=97;B=[];
for(var r=0;r<ROWS;r++)for(var c=0;c<COLS;c++){
var off=(rnd()-0.5)*0.09;
// a handful never match the batch: the quakers we sort out by hand
if(rnd()<0.055)off=(rnd()<0.5?-1:1)*(0.13+rnd()*0.10);
B.push([c,r,off,(rnd()-0.5)*0.55,0.86+rnd()*0.26,0.86+rnd()*0.24,
(rnd()-0.5)*16,(rnd()-0.5)*14,(rnd()-0.5)*12]);
}
}
function fit(){W=innerWidth;H=innerHeight;cv.width=W*dpr;cv.height=H*dpr;cx.setTransform(dpr,0,0,dpr,0,0);build();draw()}
function draw(){
var band=TE&&TE.band||0.5,GH=Math.max(210,band*H),narrow=W<640;
var pad=Math.max(26,W*0.065),gw=W-pad*2;
var top=narrow?68:92,railY=GH-40,gh=Math.max(90,railY-34-top);
var cell=Math.min(gw/COLS,gh/ROWS),gW=cell*COLS,gH=cell*ROWS;
var x0=(W-gW)/2,x1=x0+gW,gy=top+(gh-gH)/2,rx0=cell*0.40;
var cw=cell,ch=cell;
cx.clearRect(0,0,W,H);cx.fillStyle="#f7f1e7";cx.fillRect(0,0,W,H);
var cur=at(t),open=t>FC?Math.min(1,(t-FC)/0.20):0;
for(var i=0;i<B.length;i++){
var bn=B[i],o=at(t+bn[2]),bx=x0+cw*(bn[0]+0.5),by=gy+ch*(bn[1]+0.5);
var rx=rx0*bn[4],ry=rx*0.70*bn[5];
cx.save();cx.translate(bx,by);cx.rotate(bn[3]);
cx.beginPath();cx.ellipse(0,0,rx,ry,0,0,6.2832);cx.fillStyle=rgb(o,1,[bn[6],bn[7],bn[8]]);cx.fill();
cx.beginPath();cx.moveTo(-rx*0.80,ry*0.06);cx.quadraticCurveTo(0,-ry*0.34,rx*0.80,ry*0.06);
cx.lineWidth=Math.max(0.7,rx*(0.10+open*0.13));
cx.strokeStyle=open>0.02?"rgba(238,225,199,"+(0.30+open*0.55)+")":rgb(o,0.62);
cx.stroke();cx.restore();
}
// the roast clock under the grid
cx.strokeStyle="rgba(42,29,20,.16)";cx.lineWidth=1;
cx.beginPath();cx.moveTo(x0,railY);cx.lineTo(x1,railY);cx.stroke();
cx.strokeStyle="#a2593b";cx.lineWidth=2;
cx.beginPath();cx.moveTo(x0,railY);cx.lineTo(x0+gW*t,railY);cx.stroke();
cx.font=(narrow?8:9)+"px ui-monospace,Menlo,monospace";cx.textAlign="center";
for(var s2=0;s2<STOP.length;s2++){
var sx=x0+gW*STOP[s2][0],on=t>=STOP[s2][0];
cx.strokeStyle=on?"rgba(162,89,59,.7)":"rgba(42,29,20,.2)";cx.lineWidth=1;
cx.beginPath();cx.moveTo(sx,railY-4);cx.lineTo(sx,railY+4);cx.stroke();
if(!narrow){cx.fillStyle=on?"rgba(162,89,59,.8)":"rgba(42,29,20,.3)";cx.fillText(mmss(STOP[s2][7]),sx,railY+18)}
}
// first crack: a marker line that stays once it has happened
if(t>=FC){
var a=Math.min(1,(t-FC)/0.05),fx=x0+gW*FC;
cx.save();cx.globalAlpha=a*0.55;cx.strokeStyle="#a2593b";cx.lineWidth=1;cx.setLineDash([3,5]);
cx.beginPath();cx.moveTo(fx,gy-14);cx.lineTo(fx,railY-6);cx.stroke();cx.setLineDash([]);
cx.globalAlpha=a;cx.fillStyle="#a2593b";cx.textAlign=fx>W*0.6?"right":"left";
cx.font="10px ui-monospace,Menlo,monospace";
cx.fillText(narrow?"RETAK PERTAMA 8:48":"RETAK PERTAMA, 196,4 DERAJAT, 8:48",fx+(fx>W*0.6?-8:8),gy-18);
cx.restore();
}
cx.beginPath();cx.arc(x0+gW*t,railY,3.4,0,6.2832);cx.fillStyle="#a2593b";cx.fill();
readout();
}
// level, Agtron, drum temperature and elapsed time at this point of the
// scroll. Lifted out of draw() so the three.js drum reports the same batch
// without a 2D canvas to write it on.
function readout(){
var narrow=innerWidth<640,cur=at(t);
lvlEl.innerHTML="<b>"+cur.n+"</b><br>Agtron "+Math.round(cur.a)+(narrow?"":"<br>Batch 0419, Gayo Wine");
tmpEl.innerHTML=Math.round(cur.T)+" derajat<br>"+mmss(cur.s)+(narrow?"":"<br>Drum 6 kg, muatan 4,2 kg");
if(t>0.02&&!hid){hid=true;hint.style.opacity="0"}
}
// Three.js, tier 1 and 2: the drum itself, seen through the sight glass.
// One hundred and twenty beans are real bodies in there. They fall, they
// pile, the wall and the three vanes carry them up and they cascade back
// down, and every one of them darkens along the continuum at its own rate,
// so the quakers stay pale while the batch goes past first crack. The rail
// under the drum keeps the roast clock, its stops and their times.
var RD=2.3,DL=3.1,NB=120,BR=0.148;
function initGL(){
var bn=[],mesh=null,drum=null,gauge=null,prog=null,knob=null,fcMark=null,fcLab=null,
fcWide=null,fcThin=null,ticks=[],tickOn=null,tickOff=null,stamps=[],
phase=0,lastT=-1,MT=null,QT=null,PV=null,SV=null,EU=null;
var RAILW=5.0;
return TE3({
tier:2,clear:0xf7f1e7,fov:30,dist:5.4,fit:false,maxDpr:1.2,
build:function(k){
var T=k.THREE,i,j;
MT=new T.Matrix4();QT=new T.Quaternion();PV=new T.Vector3();SV=new T.Vector3();EU=new T.Euler();
drum=new T.Group();k.root.add(drum);
gauge=new T.Group();k.root.add(gauge);
// the drum: the barrel we look into, its back plate and its three vanes
var shell=new T.Mesh(new T.CylinderGeometry(RD,RD,DL,44,1,true),
new T.MeshLambertMaterial({color:0x7d6a58,side:T.BackSide}));
shell.rotation.x=Math.PI/2;drum.add(shell);
var backM=new T.Mesh(new T.CircleGeometry(RD,44),new T.MeshLambertMaterial({color:0x3a2c22}));
backM.position.z=-DL/2;drum.add(backM);
var vaneM=new T.MeshLambertMaterial({color:0x5a4a3c});
var vanes=new T.Group();drum.add(vanes);
for(i=0;i<3;i++){
var vn=new T.Mesh(new T.BoxGeometry(0.26,0.05,DL*0.94),vaneM);
var a0=i*2.0944;
vn.position.set(Math.cos(a0)*(RD-0.13),Math.sin(a0)*(RD-0.13),0);
vn.rotation.z=a0;drum.add(vn);
}
var rim=new T.Mesh(new T.TorusGeometry(RD+0.04,0.075,10,48),
new T.MeshLambertMaterial({color:0x2a1d14}));
rim.position.z=DL/2;drum.add(rim);
// one bean, modelled once: an ellipsoid with the crease cut into it
var bg=new T.SphereGeometry(1,14,10),pa=bg.attributes.position;
for(i=0;i<pa.count;i++){
var x=pa.getX(i),y=pa.getY(i)*0.60,z=pa.getZ(i)*0.80;
if(z>0)z-=Math.exp(-Math.pow(y/0.15,2))*0.40*z;
pa.setXYZ(i,x,y,z);
}
bg.computeVertexNormals();
bg.scale(BR*1.32,BR*1.32,BR*1.32);
mesh=new T.InstancedMesh(bg,new T.MeshLambertMaterial({}),NB);
mesh.instanceMatrix.setUsage(T.DynamicDrawUsage);
drum.add(mesh);
// the batch, with the same spread and the same handful of quakers the
// 2D stage sorts out by hand
sd=97;
for(i=0;i<NB;i++){
var off=(rnd()-0.5)*0.09;
if(rnd()<0.055)off=(rnd()<0.5?-1:1)*(0.13+rnd()*0.10);
var an=rnd()*6.2832,rr=Math.sqrt(rnd())*(RD-BR-0.05);
bn.push({o:off,x:Math.cos(an)*rr,y:Math.sin(an)*rr-RD*0.35,vx:0,vy:0,
z:(rnd()-0.5)*(DL-0.5),sx:0.88+rnd()*0.24,sy:0.88+rnd()*0.2,
rx:rnd()*6.2832,ry:rnd()*6.2832,rz:rnd()*6.2832,pop:false});
}
// the roast clock, its stops and its first crack, under the drum
tickOn=new T.MeshBasicMaterial({color:0xa2593b});
tickOff=new T.MeshBasicMaterial({color:0xcdc2b0});
var rail=new T.Mesh(new T.BoxGeometry(RAILW,0.028,0.05),tickOff);
rail.position.y=-RD-0.62;gauge.add(rail);
prog=new T.Mesh(new T.BoxGeometry(1,0.05,0.06),tickOn);
prog.position.y=-RD-0.62;gauge.add(prog);
knob=new T.Mesh(new T.SphereGeometry(0.07,12,10),tickOn);
knob.position.y=-RD-0.62;gauge.add(knob);
function plate(txt,fs,col){
var mc=document.createElement("canvas"),mg=mc.getContext("2d");
if(!mg)return null;
var FT=fs+"px ui-monospace,Menlo,monospace";mg.font=FT;
var tw=Math.max(8,Math.ceil(mg.measureText(txt).width))+8;
var cn=document.createElement("canvas");cn.width=tw;cn.height=Math.round(fs*1.5);
var g2=cn.getContext("2d");
if(!g2)return null;
g2.font=FT;g2.fillStyle=col;g2.textAlign="center";
g2.fillText(txt,tw/2,fs*1.1);
var tx=new T.CanvasTexture(cn);
if(T.SRGBColorSpace)tx.colorSpace=T.SRGBColorSpace;
var sp=new T.Sprite(new T.SpriteMaterial({map:tx,transparent:true,depthTest:false}));
sp.renderOrder=6;
return {sp:sp,ar:tw/cn.height};
}
for(i=0;i<STOP.length;i++){
var tk=new T.Mesh(new T.BoxGeometry(0.028,0.2,0.06),tickOff);
tk.position.set(-RAILW/2+RAILW*STOP[i][0],-RD-0.62,0.01);gauge.add(tk);ticks.push(tk);
var st=plate(mmss(STOP[i][7]),30,"rgba(42,29,20,.55)");
if(!st)return false;
st.sp.position.set(-RAILW/2+RAILW*STOP[i][0],-RD-0.96,0.02);
gauge.add(st.sp);stamps.push(st);
}
fcMark=new T.Mesh(new T.BoxGeometry(0.022,0.86,0.05),
new T.MeshBasicMaterial({color:0xa2593b,transparent:true,opacity:0.55}));
fcMark.position.set(-RAILW/2+RAILW*FC,-RD-0.28,0.01);gauge.add(fcMark);
fcWide=plate("RETAK PERTAMA, 196,4 DERAJAT, 8:48",30,"#a2593b");
fcThin=plate("RETAK PERTAMA 8:48",30,"#a2593b");
if(!fcWide||!fcThin)return false;
fcLab=fcWide.sp;
fcWide.sp.position.set(-RAILW/2+RAILW*FC+1.35,-RD-0.45,0.03);
fcThin.sp.position.set(-RAILW/2+RAILW*FC+0.8,-RD-0.45,0.03);
gauge.add(fcWide.sp);gauge.add(fcThin.sp);
stamps.push(fcWide);stamps.push(fcThin);
k.scene.add(new T.AmbientLight(0xfff1dc,1.05));
var key=new T.DirectionalLight(0xfff3e0,1.7);key.position.set(2,5,8);k.scene.add(key);
var fill=new T.DirectionalLight(0xa2593b,0.5);fill.position.set(-5,-2,3);k.scene.add(fill);
drum.rotation.set(-0.20,0.44,0);
return true;
},
layout:function(k){
var s=Math.min(k.band*k.visH*0.94/6.2,k.visW*(k.narrow?1.0:0.9)/5.4);
k.root.scale.setScalar(s);
k.root.position.y=(0.5-k.band*0.5)*k.visH+s*0.62;
var lz=0.034/Math.max(0.02,s);
for(var i=0;i<stamps.length;i++)stamps[i].sp.scale.set(lz*stamps[i].ar,lz,1);
for(var j=0;j<STOP.length;j++)stamps[j].sp.visible=!k.narrow;
fcWide.sp.visible=!k.narrow&&t>=FC;fcThin.sp.visible=k.narrow&&t>=FC;
fcLab=k.narrow?fcThin.sp:fcWide.sp;
},
onScroll:function(k){t=k.t;readout();},
frame:function(dt,k){
var T=k.THREE,i,j,d=Math.min(48,dt);
if(t!==lastT){
lastT=t;
var open=t>FC?Math.min(1,(t-FC)/0.20):0;
for(i=0;i<NB;i++){
var o=at(t+bn[i].o);
mesh.setColorAt(i,new T.Color(o.r/255,o.g/255,o.b/255));
// first crack: the bean jumps and swells the moment it cracks
var cracked=(t+bn[i].o)>=FC;
if(cracked&&!bn[i].pop){bn[i].pop=true;
bn[i].vx+=(rnd()-0.5)*0.022;bn[i].vy+=rnd()*0.016}
if(!cracked)bn[i].pop=false;
}
if(mesh.instanceColor)mesh.instanceColor.needsUpdate=true;
mesh.userData.open=open;
fcMark.visible=t>=FC;
fcWide.sp.visible=!k.narrow&&t>=FC;fcThin.sp.visible=k.narrow&&t>=FC;
prog.scale.x=Math.max(0.001,t*RAILW);
prog.position.x=-RAILW/2+t*RAILW/2;
knob.position.x=-RAILW/2+t*RAILW;
for(j=0;j<STOP.length;j++)ticks[j].material=t>=STOP[j][0]?tickOn:tickOff;
}
// the drum turns, and the beds falls, packs and cascades inside it
var w=0.0016;
phase+=w*d;
for(i=0;i<3;i++)drum.children[3+i].rotation.z=i*2.0944+phase;
var Rw=RD-BR-0.02,g2=0.000042;
for(i=0;i<NB;i++){
var b=bn[i];
b.vy-=g2*d;
b.x+=b.vx*d;b.y+=b.vy*d;
}
// beans are bodies, so they take up room: two passes of separation
for(var pass=0;pass<2;pass++){
for(i=0;i<NB;i++){
var a=bn[i];
for(j=i+1;j<NB;j++){
var c=bn[j],dx=c.x-a.x,dy=c.y-a.y,d2=dx*dx+dy*dy;
if(d2>0.0876||d2<1e-9)continue;
var dd=Math.sqrt(d2),ov=(0.296-dd)*0.5,ux=dx/dd,uy=dy/dd;
a.x-=ux*ov;a.y-=uy*ov;c.x+=ux*ov;c.y+=uy*ov;
var rel=(c.vx-a.vx)*ux+(c.vy-a.vy)*uy;
if(rel<0){a.vx+=ux*rel*0.4;a.vy+=uy*rel*0.4;c.vx-=ux*rel*0.4;c.vy-=uy*rel*0.4}
}
}
}
for(i=0;i<NB;i++){
var bb=bn[i];
// the three vanes lift what they catch
for(j=0;j<3;j++){
var pa2=phase+j*2.0944,ca=Math.cos(pa2),sa=Math.sin(pa2);
var al=bb.x*ca+bb.y*sa,pe=-bb.x*sa+bb.y*ca;
if(al>Rw-0.3&&al<Rw+0.1&&Math.abs(pe)<BR){
pe=(pe>=0?1:-1)*BR;
bb.x=al*ca-pe*sa;bb.y=al*sa+pe*ca;
bb.vx+=(-w*bb.y*1000*0.001-bb.vx)*0.45;
bb.vy+=(w*bb.x*1000*0.001-bb.vy)*0.45;
}
}
var dl=Math.sqrt(bb.x*bb.x+bb.y*bb.y);
if(dl>Rw){
var nx=bb.x/dl,ny=bb.y/dl;
bb.x=nx*Rw;bb.y=ny*Rw;
var vr=bb.vx*nx+bb.vy*ny;
if(vr>0){bb.vx-=vr*nx*1.2;bb.vy-=vr*ny*1.2}
bb.vx+=(-w*bb.y-bb.vx)*0.22;bb.vy+=(w*bb.x-bb.vy)*0.22;
}
bb.vx*=0.995;bb.vy*=0.995;
var sp2=Math.sqrt(bb.vx*bb.vx+bb.vy*bb.vy);
bb.rz+=(bb.vx*0.9+sp2*0.4)*d;bb.rx+=sp2*0.22*d;bb.ry+=sp2*0.14*d;
var sw=1+(mesh.userData.open||0)*0.13;
EU.set(bb.rx,bb.ry,bb.rz);QT.setFromEuler(EU);
PV.set(bb.x,bb.y,bb.z);SV.set(bb.sx*sw,bb.sy*sw,bb.sx*sw);
MT.compose(PV,QT,SV);mesh.setMatrixAt(i,MT);
}
mesh.instanceMatrix.needsUpdate=true;
drum.rotation.y=0.44+k.px*0.07;
drum.rotation.x=-0.20-k.py*0.04;
}
});
}
if(!initGL()){
fit();addEventListener("resize",fit);
if(TE&&TE.onCopy)TE.onCopy(draw);
(function(){
function onScroll(v){t=v<0?0:v>1?1:v;draw()}
if(TE&&TE.onScroll)TE.onScroll(onScroll);else addEventListener("message",function(e){if(e.data&&e.data.type==="te:scroll")onScroll(e.data.t)});
})();
}
})();
</script></body></html>
Sangrai kecil, Bandung Utara
Setiap kantong membawa kurva sangrainya
Gulir dan seratus dua puluh biji di atas melewati sangrai yang sama seperti drum kami: hijau, kuning, retak pertama pada 196,4 derajat di menit 8:48, lalu terang sampai gelap. Beberapa biji tidak ikut matang, dan itu memang selalu terjadi.
Drum 6 kg, muatan 4,2 kgProfil dicetak di tiap kantongSangrai Selasa dan Jumat
4,2
kilogram hijau per batch, tidak pernah lebih
14
batch per minggu, itu kapasitas penuhnya
8:48
menit rata-rata sampai retak pertama
6
biji di daftar, sama sejak 2021
Enam biji, dengan ketinggian dan prosesnya
Gayo WinePanen 2025
Rp168.000 / 200 g
Aceh Tengah, 1.450 mdpl, varietas Ateng Super. Natural anaerob 96 jam di dalam tong tertutup. Kami sangrai sampai Agtron 68, retak pertama 8:41, pengembangan 1 menit 52 detik. Manis buah merah dengan fermentasi yang terasa, dan kami tidak menyembunyikannya.
Malabar Honey
Rp132.000 / 200 g
Pangalengan, 1.550 mdpl, varietas Sigararutang. Proses honey, lendir disisakan 60 persen, dijemur sebelas hari di para para. Agtron 62, retak pertama 8:48. Gula aren dan jeruk keprok.
Kerinci Sungai Penuh
Rp124.000 / 200 g
Jambi, 1.500 mdpl, varietas Andungsari. Full washed, fermentasi 36 jam. Agtron 64. Biji paling jinak di daftar ini, dan yang paling sering kami sarankan untuk orang yang baru menyeduh sendiri.
Toraja Sapan
Rp138.000 / 200 g
Sulawesi Selatan, 1.600 mdpl, Typica dan S795 yang tercampur di kebun dan tidak kami pisahkan. Full washed. Agtron 60, pengembangan 2 menit 04 detik. Rempah kering dan cokelat pahit.
Flores Bajawa
Rp118.000 / 200 g
Ngada, 1.400 mdpl, varietas Juria dan Lini S. Natural, dijemur sembilan hari. Agtron 58. Paling tebal di badan, dan paling tidak cocok untuk V60 menurut kami sendiri.
Preanger RobustaUntuk kopi susu
Rp78.000 / 250 g
Garut, 1.100 mdpl, robusta washed. Agtron 38, sangrai paling gelap yang kami kerjakan. Dibuat untuk susu, bukan untuk diseduh tubruk. Stoknya paling sering habis hari Kamis.
Dari enam kebun, semuanya di sabuk kopi
Kenapa kami mencetak profil yang meleset
Bulan Agustus 2020 kami mengirim dua belas kilogram Malabar ke empat kedai. Batch itu keluar pada 213 derajat, tujuh derajat lebih panas dari yang tertulis, karena termokopel kami bergeser dan tidak ada yang memeriksanya. Tidak ada yang komplain. Tiga kedai memakainya sampai habis dan satu memuji rasanya.
Itu yang membuat kami takut. Kalau kesalahan tujuh derajat bisa lewat tanpa ada yang tahu, maka semua yang kami katakan tentang ketelitian hanya cerita. Sejak batch berikutnya kurva dicetak di kantong, lengkap dengan target dan hasil sebenarnya, dan batch yang meleset lebih dari dua Agtron dijual dengan potongan dua puluh persen serta tanda bintang di kodenya.
Sampai hari ini ada tiga puluh satu batch bertanda bintang dari 1.480 batch. Daftarnya ada di gudang, ditempel di dinding, dan siapa pun boleh memotretnya.
Satu hari sangrai, dari Senin sampai kopi boleh diseduh
01
Senin, sortir dengan tangan
Tiga orang menyortir karung enam puluh kilogram di atas meja putih. Rata-rata empat biji per kilogram tidak lolos, biji hitam, pecah, atau berlubang bubuk. Ini pekerjaan yang paling sering membuat satu batch mundur sehari, dan kami belum menemukan cara mempercepatnya tanpa mengorbankan hasilnya.
02
Selasa 06.30, menimbang
Hijau ditimbang 4,2 kilogram per batch, tidak pernah lebih. Selisih di atas dua puluh gram masuk batch berikutnya, bukan dipaksakan ke dalam drum yang sama.
03
Selasa 07.10, drum dipanaskan
Drum enam kilogram dipanaskan sampai 195 derajat lalu ditahan dua belas menit supaya seluruh dindingnya rata. Batch pertama hari itu selalu kami anggap batch kalibrasi dan tidak dijual sebagai kopi utama.
04
Selasa 07.30, batch pertama masuk
Suhu jatuh ke 92 derajat, titik balik pada 1 menit 28 detik. Dari sini suhu dicatat tiap lima belas detik, dengan tangan, di kertas yang sama yang nanti difoto untuk kantong.
05
Menit 8:48, retak pertama
196,4 derajat. Setelah bunyi itu kami punya kira kira satu menit lima puluh detik untuk memutuskan mau berhenti di mana, dan keputusan itu ditulis sebelum diambil, bukan diingat sesudahnya.
06
Empat hari diam
Kopi tidak dijual di hari yang sama. Empat hari untuk seduh manual, tujuh hari untuk espresso. Kantong yang keluar hari Selasa adalah hasil sangrai Jumat minggu lalu.
Kurva Malabar Honey, dicetak di kantong dan berdiri di sini
Kami ganti pemasok tiga kali sebelum Kalibrasi. Yang membuat kami berhenti mencari bukan rasanya, tapi kurva yang datang bersama karung. Barista saya sudah tahu mau menyetel gilingan ke mana sebelum kopinya sampai.Dimas Prayoga, Kedai Ruang Tamu, Dago
Batch 0311 meleset dan mereka yang memberi tahu duluan lewat pesan, sebelum kami sempat menyeduh. Uangnya kembali, kopinya tetap kami pakai untuk latihan barista.Sarah Wijaya, Sekawan Coffee, Buah Batu
Saya beli dua ratus gram tiap dua minggu sejak 2021. Satu-satunya keluhan saya, Preanger Robusta hampir selalu habis kalau saya datang hari Kamis sore.Bagus Nurhadi, pelanggan sejak 2021
Saya bawa biji dari kebun mertua dan ditolak. Alasannya dijelaskan tujuh menit dan saya pulang mengerti kenapa, lalu diberi nama penyangrai lain yang mau.Tania Larasati, Cimahi
Yang paling sering ditanyakan di meja pengambilan
Kenapa hanya sangrai Selasa dan Jumat
Satu drum 6 kilogram dan satu penyangrai. Tujuh batch sehari adalah batas sebelum dinding drum tidak sempat kembali rata, dan batch kedelapan selalu lebih gelap dari yang tertulis. Jadi empat belas batch seminggu, dan pesanan di atas itu masuk minggu berikutnya.
Apa arti angka di kantong saya
Baris pertama kode batch dan tanggal sangrai. Baris kedua Agtron, waktu retak pertama, dan lama pengembangan setelahnya. Baris ketiga suhu keluar. Kalau ada tanda bintang, berarti batch itu meleset lebih dari dua Agtron dari target dan harganya kami potong dua puluh persen.
Bisa menyangrai biji yang saya bawa sendiri
Tidak. Kami hanya menyangrai biji yang kami beli sendiri dari enam kebun yang sama sejak 2021, karena kami tidak bisa menuliskan kurva untuk biji yang riwayat pascapanennya tidak kami ketahui. Ada tiga penyangrai lain di Bandung yang menerima jasa itu dan kami akan menyebut namanya kalau ditanya.
Berapa lama kopi harus didiamkan
Empat hari untuk seduh manual, tujuh hari untuk espresso. Kami menuliskan tanggal paling awal untuk menyeduh di kantong, bukan tanggal kedaluwarsa. Setelah lima minggu rasanya turun dan kami lebih suka mengatakannya daripada tidak.
Ada tempat duduk di sini
Tidak ada. Cigadung Raya Barat 42 adalah tempat sangrai dan gudang, dengan satu meja untuk mengambil pesanan. Kami menyeduh untuk pelanggan yang mengambil sendiri, satu cangkir, gratis, selama masih ada air panas.
Kirim ke luar Bandung
Ke seluruh Indonesia lewat kurir, dikemas dengan katup satu arah dan dikirim di hari sangrai. Untuk Bandung kami antar sendiri hari Rabu dan Sabtu, minimum dua kilogram, tanpa biaya tambahan.
Pesan sebelum Senin sore untuk sangrai Selasa
Jalan Cigadung Raya Barat 42, Bandung Utara
Sangrai Selasa dan Jumat 07.00 sampai 15.00. Pengambilan Senin sampai Sabtu 10.00 sampai 17.00, Minggu tutup.