• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
01Critter Models 02
└ Locust└ Butterfly└ Cockroach└ Spider└ Scorpion└ Dragon└ Brazilian Treehopper└ Tardigrade└ Diatoms└ Bumblebee└ Wasp└ Hammerhead Worm└ Giraffe Weevil└ Trilobite Beetle
02VantaJS Shaders
└ Clouds└ Waves└ Cells└ Rings└ Halo└ Globe└ Net└ Dots└ Birds└ Trunk└ Topology└ About VantaJS
03App Bundle 02
└ Gutindex└ Insect Builder└ OpenLayers
04Cathedrals
└ Lattice└ Lost└ Lucent└ Lumen└ Glass└ Disco└ Hyperion└ Latent
05App Bundle 01
└ Robohash└ PrismJS└ Horoscope└ Binary Jazz└ Morse Code└ URL Extractor└ Entity Escaper└ Phonetic Alphabet└ Toki Pona└ Semaphore└ Cut Up Machine└ Text Mixer└ Currency Freaks└ Host IO└ CrUX API└ Logo Dev└ Web Speech API└ GEO DB
06Retroscopic
└ Galaxy└ Ocean└ Slime└ 24-Cell└ Unicorn└ Polyp└ Oneiric 1└ Oneiric 2└ Node└ Torus Knot└ GLSL Grid└ Liquid Crystal└ Silky└ Butterfly└ Ocean 2└ Icosahedron└ Luma└ Swarm└ Broken Solid└ Hypersphere└ Constraints└ Psycho Killer
01 / 03

COCKROACH - THREEJS PROCEDURAL ANIMATION SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ThreeJS: Cockroach</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { background: #000; overflow: hidden; height: 100%; }
  #bgCanvas { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 1; }
  #animationCanvas { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 2; cursor: grab; touch-action: none; }
  #animationCanvas:active { cursor: grabbing; }
</style>
</head>
<body>
<canvas id="bgCanvas"></canvas>
<canvas id="animationCanvas"></canvas>

<script>
const COL = {
  wing:       0x5d2c14,
  wingEdge:   0x7a3d1c,
  carapace:   0x6b3517,
  pronotDark: 0x2c150a,
  pronotEdge: 0xba8236,
  belly:      0x8a5528,
  leg:        0x4a2513,
  legLight:   0x6a3a1e,
  legDark:    0x35190c,
  spine:      0x1c0e06,
  antenna:    0x3d1d0e,
  eye:        0x0a0503,
  eyeGloss:   0x5a3016,
  cerci:      0x774223
};

const bgCanvas = document.getElementById('bgCanvas');
const bgCtx = bgCanvas.getContext('2d');
const pixelSize = 9;
let cols, rows, pix = [];
function initBackground(){
  bgCanvas.width = innerWidth; bgCanvas.height = innerHeight;
  bgCtx.fillStyle = '#000';
  bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);
}

const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('animationCanvas'), antialias:true, alpha:true });
let scene, camera, roach;
const legParts = [];
const antennaJoints = [];
const palps = [];
let wings = [];

function updateSize(){
  renderer.setSize(innerWidth, innerHeight);
  renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
}

const cam = { dist: 8.5, yaw: 0.5, pitch: 0.32, targetYaw: 0.5, targetPitch: 0.32, targetDist: 8.5, autoYaw: 0 };
function updateCamera(){
  const w = innerWidth;
  if (w < 480) cam.targetDist = Math.min(Math.max(cam.targetDist, 8.5), 13);
  camera.fov = w < 480 ? 58 : w < 768 ? 52 : 46;
  camera.updateProjectionMatrix();
}
function applyCamera(){
  cam.yaw   += (cam.targetYaw   - cam.yaw)   * 0.08;
  cam.pitch += (cam.targetPitch - cam.pitch) * 0.08;
  cam.dist  += (cam.targetDist  - cam.dist)  * 0.08;
  const total = cam.yaw + cam.autoYaw;
  const p = Math.max(-1.35, Math.min(1.35, cam.pitch));
  camera.position.set(
    Math.sin(total)*Math.cos(p)*cam.dist,
    Math.sin(p)*cam.dist + 0.4,
    Math.cos(total)*Math.cos(p)*cam.dist
  );
  camera.lookAt(0, -0.1, -0.1);
}

function chitin(color, opts={}){
  return new THREE.MeshStandardMaterial(Object.assign({
    color, roughness:0.42, metalness:0.28, side:THREE.DoubleSide
  }, opts));
}

const iriMats = [];
let hueShift = 0;
function iri(offset, opts={}){
  const { sat=0.85, light=0.52, roughness=0.30, metalness=0.55, opacity=1, transparent=false } = opts;
  const m = new THREE.MeshStandardMaterial({
    color:0xffffff, roughness, metalness, side:THREE.DoubleSide, transparent, opacity
  });
  m.userData = { offset, sat, light };
  m.color.setHSL(((offset%1)+1)%1, sat, light);
  iriMats.push(m);
  return m;
}

function segmentBetween(p0, p1, r0, r1, material, radialSeg=10){
  const dir = new THREE.Vector3().subVectors(p1, p0);
  const len = dir.length();
  const geo = new THREE.CylinderGeometry(r1, r0, len, radialSeg, 1, false);
  const mesh = new THREE.Mesh(geo, material);
  mesh.position.copy(p0).add(p1).multiplyScalar(0.5);
  mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0), dir.clone().normalize());
  return mesh;
}

function makeLeg(side, cfg){
  const s = side;
  const h = cfg.hue;
  const legMat  = iri(h,        { roughness:0.34, metalness:0.5 });
  const legMat2 = iri(h+0.05,   { roughness:0.30, metalness:0.55, light:0.56 });
  const darkMat = iri(h-0.04,   { roughness:0.4,  metalness:0.45, light:0.4 });
  const spineMat= chitin(0x140b09, { roughness:0.6, metalness:0.1 });

  const Lf = cfg.fem;
  const Lt = cfg.tib;
  const femDroop = 0.55;
  const kneeBend = 1.05;

  const root = new THREE.Group();
  root.position.set(s*cfg.hipX, cfg.hipY, cfg.hipZ);
  root.rotation.y = cfg.yaw;

  const coxaEnd = new THREE.Vector3(s*0.16, -0.04, 0);
  root.add(segmentBetween(new THREE.Vector3(0,0,0), coxaEnd, 0.055, 0.05, darkMat, 8));

  const femur = new THREE.Group();
  femur.position.copy(coxaEnd);
  femur.rotation.z = -s*femDroop;
  root.add(femur);
  const femEnd = new THREE.Vector3(s*Lf, 0, 0);
  femur.add(segmentBetween(new THREE.Vector3(0,0,0), femEnd, 0.05, 0.036, legMat, 10));

  const knee = new THREE.Group();
  knee.position.copy(femEnd);
  knee.rotation.z = -s*kneeBend;
  femur.add(knee);
  const tibEnd = new THREE.Vector3(s*Lt, 0, 0);
  knee.add(segmentBetween(new THREE.Vector3(0,0,0), tibEnd, 0.034, 0.020, legMat2, 9));

  function addSpines(parent, a, b, count, rad){
    const along = new THREE.Vector3().subVectors(b, a).normalize();
    for (let i=1;i<=count;i++){
      const t = i/(count+1);
      const base = new THREE.Vector3().lerpVectors(a, b, t);
      const tip = base.clone()
        .add(new THREE.Vector3(0,-rad,0))
        .add(along.clone().multiplyScalar(-rad*0.5*s));
      parent.add(segmentBetween(base, tip, rad*0.45, 0.001, spineMat, 4));
    }
  }
  addSpines(femur, new THREE.Vector3(0,0,0), femEnd, 3, 0.055);
  addSpines(knee,  new THREE.Vector3(0,0,0), tibEnd, 6, 0.06);

  let prev = tibEnd.clone();
  for (let k=0;k<5;k++){
    const nt = prev.clone().add(new THREE.Vector3(s*(0.045-k*0.004), -0.045 - k*0.006, 0.0));
    knee.add(segmentBetween(prev, nt, 0.020 - k*0.002, 0.016 - k*0.002, darkMat, 6));
    prev = nt;
  }
  [-1,1].forEach(cs=>{
    const claw = prev.clone().add(new THREE.Vector3(s*0.03, -0.03, cs*0.03));
    knee.add(segmentBetween(prev, claw, 0.012, 0.001, spineMat, 4));
  });

  legParts.push({
    root, femur, knee, side:s,
    baseYaw:cfg.yaw, baseFemur:-s*femDroop, baseKnee:-s*kneeBend,
    phase:cfg.phase, swing:cfg.swing
  });
  return root;
}

function makeAntenna(side){
  const root = new THREE.Group();
  root.position.set(side*0.16, 0.16, 1.42);
  root.rotation.set(-0.35, side*0.45, 0);
  const joints = [];
  const N = 18;
  let parent = root;
  for (let i=0;i<N;i++){
    const t = i/N;
    const antMat = iri(t*0.9, { roughness:0.4, metalness:0.4, light:0.55 });
    const j = new THREE.Group();
    j.userData.bx = -0.06 - 0.02*Math.sin(i*0.6);
    j.userData.by = side*0.02;
    j.rotation.set(j.userData.bx, j.userData.by, 0);
    const segLen = 0.30*(1 - t*0.55);
    const r0 = 0.030*(1-t) + 0.006;
    const r1 = 0.030*(1-(t+1/N)) + 0.006;
    j.add(segmentBetween(new THREE.Vector3(0,0,0), new THREE.Vector3(0,0,segLen), r0, r1, antMat, 6));
    const ring = new THREE.Mesh(new THREE.TorusGeometry(r1*1.15, r1*0.35, 5, 8), antMat);
    ring.position.set(0,0,segLen); ring.rotation.x = Math.PI/2;
    j.add(ring);
    parent.add(j);
    const holder = new THREE.Group();
    holder.position.set(0,0,segLen);
    j.add(holder);
    parent = holder;
    joints.push(j);
  }
  antennaJoints.push({ joints, side });
  return root;
}

function createRoach(){
  const g = new THREE.Group();

  const abdomen = new THREE.Group();
  const nSeg = 8;
  for (let i=0;i<nSeg;i++){
    const t = i/(nSeg-1);
    const w = 0.62 * (1 - t*0.5);
    const segHue = 0.42 + t*0.55;
    const geo = new THREE.SphereGeometry(1, 20, 12);
    const seg = new THREE.Mesh(geo, iri(segHue, { light: i%2?0.55:0.46, metalness:0.55 }));
    seg.scale.set(w, 0.20*(1-t*0.25), 0.30);
    seg.position.set(0, -0.05 - t*0.05, -0.35 - i*0.28);
    abdomen.add(seg);
    const belly = new THREE.Mesh(geo, iri(segHue-0.03, { light:0.6, metalness:0.45 }));
    belly.scale.set(w*0.9, 0.14, 0.26);
    belly.position.set(0, -0.16 - t*0.05, -0.35 - i*0.28);
    abdomen.add(belly);
    if (i<nSeg-1) [-1,1].forEach(sx=>{
      const b = new THREE.Mesh(new THREE.SphereGeometry(0.045,8,8), iri(segHue+0.05,{light:0.62}));
      b.position.set(sx*w*0.98, -0.08 - t*0.05, -0.35 - i*0.28);
      abdomen.add(b);
    });
  }
  [-1,1].forEach(sx=>{
    const p0 = new THREE.Vector3(sx*0.14, -0.14, -0.35 - (nSeg-1)*0.28);
    const p1 = new THREE.Vector3(sx*0.30, -0.20, -0.35 - (nSeg-1)*0.28 - 0.55);
    abdomen.add(segmentBetween(p0, p1, 0.05, 0.005, iri(0.98,{roughness:0.4,metalness:0.5}), 6));
  });
  g.add(abdomen);

  const thorax = new THREE.Mesh(new THREE.SphereGeometry(0.55, 24, 18), iri(0.30,{roughness:0.3,metalness:0.55}));
  thorax.scale.set(1, 0.42, 1.05);
  thorax.position.set(0, 0.02, 0.5);
  g.add(thorax);

  const shieldGroup = new THREE.Group();
  const marginGeo = new THREE.SphereGeometry(0.72, 32, 20);
  const margin = new THREE.Mesh(marginGeo, iri(0.14,{roughness:0.25,metalness:0.6,light:0.55}));
  margin.scale.set(1.0, 0.32, 0.92);
  shieldGroup.add(margin);
  const centreGeo = new THREE.SphereGeometry(0.58, 32, 20);
  const centre = new THREE.Mesh(centreGeo, iri(0.06,{roughness:0.22,metalness:0.7,light:0.48}));
  centre.scale.set(1.0, 0.40, 0.9);
  centre.position.set(0, 0.055, -0.02);
  shieldGroup.add(centre);
  [-1,1].forEach(sx=>{
    const lobe = new THREE.Mesh(new THREE.SphereGeometry(0.16,16,12), iri(0.18,{roughness:0.25,light:0.6}));
    lobe.scale.set(1,0.4,1.2);
    lobe.position.set(sx*0.2, 0.19, 0.1);
    shieldGroup.add(lobe);
  });
  shieldGroup.position.set(0, 0.16, 0.78);
  shieldGroup.rotation.x = -0.06;
  g.add(shieldGroup);

  const head = new THREE.Group();
  const skull = new THREE.Mesh(new THREE.SphereGeometry(0.26, 24, 20), iri(0.0,{roughness:0.28,metalness:0.55}));
  skull.scale.set(1, 1.15, 0.85);
  head.add(skull);
  [-1,1].forEach(sx=>{
    const eye = new THREE.Mesh(new THREE.SphereGeometry(0.13, 18, 16), chitin(0x0a0503,{roughness:0.15,metalness:0.6}));
    eye.scale.set(0.75, 1.1, 0.7);
    eye.position.set(sx*0.2, 0.05, 0.06);
    head.add(eye);
    const gloss = new THREE.Mesh(new THREE.SphereGeometry(0.05,10,10), chitin(0xffffff,{roughness:0.1,metalness:0.3}));
    gloss.position.set(sx*0.24, 0.11, 0.12);
    head.add(gloss);
  });
  const face = new THREE.Mesh(new THREE.SphereGeometry(0.14,16,12), iri(0.94,{roughness:0.35,light:0.42}));
  face.scale.set(1,0.8,0.7); face.position.set(0,-0.18,0.12);
  head.add(face);
  [-1,1].forEach(sx=>{
    const palp = new THREE.Group();
    palp.position.set(sx*0.12, -0.22, 0.14);
    let prev = new THREE.Vector3(0,0,0);
    for (let k=0;k<3;k++){
      const nt = prev.clone().add(new THREE.Vector3(sx*0.03, -0.05, 0.07));
      palp.add(segmentBetween(prev, nt, 0.028-k*0.006, 0.02-k*0.006, iri(0.9+k*0.03,{roughness:0.4}),5));
      prev = nt;
    }
    head.add(palp);
    palps.push({ palp, side:sx });
  });
  head.position.set(0, 0.02, 1.28);
  head.rotation.x = 0.5;
  g.add(head);

  g.add(makeAntenna(-1));
  g.add(makeAntenna( 1));

  function makeWing(side){
    const len = 2.5, wid = 0.62;
    const shape = new THREE.Shape();
    shape.moveTo(0,0);
    shape.bezierCurveTo(len*0.18, wid*0.55, len*0.5, wid*0.72, len*0.72, wid*0.6);
    shape.bezierCurveTo(len*0.88, wid*0.5, len*0.99, wid*0.18, len, 0);
    shape.bezierCurveTo(len*0.99,-wid*0.28, len*0.6,-wid*0.4, len*0.25,-wid*0.28);
    shape.bezierCurveTo(len*0.1,-wid*0.18, 0.02,-wid*0.05, 0,0);
    const geo = new THREE.ExtrudeGeometry(shape, { depth:0.03, bevelEnabled:true, bevelThickness:0.02, bevelSize:0.02, bevelSegments:2, steps:1, curveSegments:24 });
    const wingMat = iri(side>0 ? 0.48 : 0.55, { roughness:0.18, metalness:0.85, light:0.55, opacity:0.9, transparent:true });
    const wing = new THREE.Mesh(geo, wingMat);
    const pivot = new THREE.Group();
    pivot.add(wing);
    wing.rotation.set(-Math.PI/2 + 0.06, 0, 0);
    wing.rotation.z = 0;
    pivot.rotation.set(0, side>0 ? -0.14 : Math.PI+0.14, 0);
    pivot.rotation.z = side>0 ? 0.03 : -0.03;
    pivot.position.set(side*0.03, 0.30, 0.62);
    for (let vi=0; vi<7; vi++){
      const off = (vi/6);
      const veinMat = iri(0.55 + off*0.4, { roughness:0.3, metalness:0.6, light:0.6 });
      const vc = new THREE.QuadraticBezierCurve3(
        new THREE.Vector3(len*0.05, off*wid*0.5*(off<0.5?1:-0.6)+0.01, 0.05),
        new THREE.Vector3(len*0.5, (off-0.4)*wid*0.9, 0.05),
        new THREE.Vector3(len*0.95, (off-0.5)*wid*0.4, 0.05)
      );
      const vg = new THREE.TubeGeometry(vc, 10, 0.010, 4, false);
      const vm = new THREE.Mesh(vg, veinMat);
      vm.rotation.copy(wing.rotation);
      pivot.add(vm);
    }
    wings.push(pivot);
    return pivot;
  }
  g.add(makeWing(1));
  g.add(makeWing(-1));

  const legCfg = [
    { hipX:0.32, hipY:-0.02, hipZ:0.86, yaw:-0.80, fem:0.60, tib:0.66, swing:0.20, phase:0, hue:0.02 },
    { hipX:0.38, hipY:-0.04, hipZ:0.30, yaw: 0.00, fem:0.74, tib:0.82, swing:0.20, phase:Math.PI, hue:0.42 },
    { hipX:0.38, hipY:-0.05, hipZ:-0.28, yaw: 0.82, fem:0.92, tib:1.02, swing:0.22, phase:0, hue:0.75 },
  ];
  legCfg.forEach(c=>{
    g.add(makeLeg( 1, c));
    g.add(makeLeg(-1, Object.assign({}, c, { yaw:-c.yaw, phase:c.phase + Math.PI })));
  });

  g.scale.setScalar(1.15);
  return g;
}

function init(){
  initBackground(); updateSize();

  scene = new THREE.Scene();
  scene.background = null;
  scene.fog = new THREE.FogExp2(0x000000, 0.03);

  camera = new THREE.PerspectiveCamera(46, innerWidth/innerHeight, 0.1, 1000);
  updateCamera();

  roach = createRoach();
  scene.add(roach);

  scene.add(new THREE.HemisphereLight(0xffffff, 0x141020, 0.6));
  const key = new THREE.DirectionalLight(0xffffff, 1.35); key.position.set(4,7,6); scene.add(key);
  const rim = new THREE.PointLight(0x33ccff, 1.5, 60); rim.position.set(-6,3,-4); scene.add(rim);
  const rim2 = new THREE.PointLight(0xff33aa, 1.3, 60); rim2.position.set(6,-2,-3); scene.add(rim2);
  const under = new THREE.PointLight(0x8855ff, 0.6, 40); under.position.set(0,-5,3); scene.add(under);
  const glint = new THREE.PointLight(0xffffff, 1.1, 40); glint.position.set(2,4,8); scene.add(glint);

  addPointerControls();
  animate();
}

let gaitSpeed = 0.9, gaitTarget = 0.9, nextBurst = 2;
function animate(){
  requestAnimationFrame(animate);
  const now = performance.now();
  const t = now * 0.001;

  hueShift = (t * 0.05) % 1;
  for (let i=0;i<iriMats.length;i++){
    const m = iriMats[i];
    m.color.setHSL((hueShift + m.userData.offset) % 1, m.userData.sat, m.userData.light);
  }

  if (t > nextBurst){
    gaitTarget = Math.random() < 0.4 ? 0.15 : 2.2 + Math.random()*2.5;
    nextBurst = t + 0.8 + Math.random()*2.2;
  }
  gaitSpeed += (gaitTarget - gaitSpeed) * 0.04;
  const walk = (animate.wt = (animate.wt||0) + gaitSpeed * 0.05);

  legParts.forEach(L=>{
    const ph = walk + L.phase;
    const swingLift = Math.max(0, Math.cos(ph));
    const ease = swingLift * swingLift * (3 - 2*swingLift);
    L.root.rotation.y  = L.baseYaw   + L.side * L.swing * Math.sin(ph);
    L.femur.rotation.z = L.baseFemur + L.side * 0.34 * ease;
    L.knee.rotation.z  = L.baseKnee  + L.side * 0.30 * ease;
  });

  antennaJoints.forEach(({joints, side})=>{
    for (let i=0;i<joints.length;i++){
      const tt = i/joints.length;
      const j = joints[i];
      j.rotation.x = j.userData.bx + Math.sin(t*3.0 - i*0.5) * 0.14 * tt;
      j.rotation.y = j.userData.by + Math.sin(t*2.3 - i*0.55 + side*1.3) * 0.20 * tt
                                   + Math.sin(t*0.7 + side) * 0.10 * tt;
    }
  });

  palps.forEach(({palp, side})=>{ palp.rotation.x = Math.sin(t*6 + side)*0.25; });

  wings.forEach((w,i)=>{ w.rotation.z = (i===0?0.03:-0.03) + Math.sin(t*1.5 + i)*0.015; });

  const busy = Math.min(1, gaitSpeed/2);
  roach.position.y = Math.sin(walk*2)*0.03*busy - 0.05;
  roach.position.x = Math.sin(walk)*0.02*busy;
  roach.rotation.z = Math.sin(walk*2)*0.02*busy;
  roach.rotation.x = Math.sin(t*0.9)*0.02;

  if (!dragging) cam.autoYaw += 0.0016 + gaitSpeed*0.00025;
  applyCamera();

  renderer.render(scene, camera);
}

let dragging=false, lastX=0, lastY=0, pinchD=0;
function addPointerControls(){
  const c = renderer.domElement;
  c.addEventListener('pointerdown', e=>{ dragging=true; lastX=e.clientX; lastY=e.clientY; c.setPointerCapture(e.pointerId); });
  c.addEventListener('pointermove', e=>{
    if(!dragging) return;
    cam.targetYaw   += (e.clientX-lastX)*0.006;
    cam.targetPitch += (e.clientY-lastY)*0.006;
    lastX=e.clientX; lastY=e.clientY;
  });
  const stop=()=>dragging=false;
  c.addEventListener('pointerup', stop);
  c.addEventListener('pointercancel', stop);
  c.addEventListener('pointerleave', stop);
  c.addEventListener('wheel', e=>{ e.preventDefault(); cam.targetDist = Math.max(4.5, Math.min(16, cam.targetDist + e.deltaY*0.01)); }, {passive:false});
  c.addEventListener('touchmove', e=>{
    if(e.touches.length===2){
      const dx=e.touches[0].clientX-e.touches[1].clientX, dy=e.touches[0].clientY-e.touches[1].clientY;
      const d=Math.hypot(dx,dy);
      if(pinchD) cam.targetDist = Math.max(4.5, Math.min(16, cam.targetDist + (pinchD-d)*0.02));
      pinchD=d;
    }
  }, {passive:true});
  c.addEventListener('touchend', ()=>pinchD=0);
}

addEventListener('resize', ()=>{
  updateSize();
  initBackground();
  if(camera){ camera.aspect = innerWidth/innerHeight; updateCamera(); }
});
addEventListener('load', init);
</script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.