# Potion Bottle

> A stylized magic potion: a lathed glass flask (round-bottom, tall bottle or conical) holding a glowing colored liquid, sealed with a cork and a twine wrap with a little paper tag. About 0.3 m tall at game-pickup scale. Set the liquid color and fill level for health, mana or poison potions in RPGs, alchemy shops and loot drops.

![Potion Bottle preview](https://codels.download/m/potion.png)

Multi-view sheet (3/4, front, side, top): https://codels.download/m/potion.sheet.png

## Facts

- Category: Items & Weapons › Consumables
- Size (W × H × D): 0.20 × 0.28 × 0.20 m — units are meters, +Y up, front faces +Z
- Origin: center of the base (y = 0)
- Triangles: 600 · Meshes: 7 · Materials: 5
- Animations: 'idle' (loop) — see "Animations" below
- License: CC0-1.0 (Codels built-in)
- Tags: alchemy, bottle, elixir, potion, cartoon, low-poly, stylized, fantasy, medieval, adventure, roguelike, rpg, consumable, pickup, prop, glass, animated, emissive, game-ready, mobile-friendly, parametric, transparent-parts, color-blue, color-red, size-tiny, poly-low
- Page: https://codels.download/models/potion

## Files

| Format | URL |
|---|---|
| three.js ES module (`createPotion()`) | https://codels.download/m/potion.js |
| React Three Fiber component (`<Potion />`) | https://codels.download/m/potion.jsx |
| Instancing module (`createPotionInstances(count, place)`, one draw call per material) | https://codels.download/m/potion.instanced.js |
| Standalone HTML demo | https://codels.download/m/potion.html |
| glTF binary | https://codels.download/m/potion.glb |
| glTF binary, Meshopt-compressed (2–3× smaller; GLTFLoader needs `setMeshoptDecoder(MeshoptDecoder)`) | https://codels.download/m/potion.glb?compress=meshopt |
| glTF JSON | https://codels.download/m/potion.gltf |
| OBJ (vertex colors) + MTL | https://codels.download/m/potion.obj · https://codels.download/m/potion.mtl |
| STL | https://codels.download/m/potion.stl |
| CodelSpec JSON | https://codels.download/m/potion.json |

## Parameters

Append them as query parameters to any URL above to get a customized variant (e.g. `?liquid=...`).

| Param | Type | Range / options | Default | Current |
|---|---|---|---|---|
| `liquid` | color | #rrggbb | #e8344e | #e8344e |
| `fill` | number | 0.15 – 1 | 0.8 | 0.8 |
| `shape` | select | round \| tall \| cone | round | round |

## Animations

The model (the .js module and the .glb/.gltf files alike) carries THREE.AnimationClip objects in `model.animations` (glTF: `gltf.animations`). The static model is the rest pose.

```js
const mixer = new THREE.AnimationMixer(model);
mixer.clipAction(THREE.AnimationClip.findByName(model.animations, 'idle')).play(); // loop
// every frame: mixer.update(deltaSeconds);
```

## Parts

- potion (group)
  - liquid (lathe · liquid)
  - glass (lathe · glass)
  - cork (cylinder · cork)
  - corkTop (cylinder · cork)
  - twine (torus · twine)
  - tagString (box · twine)
  - tag (box · paper)

## How to use

Save the code below as `potion.js`, then:

```js
import { createPotion } from './potion.js';
const model = createPotion();
scene.add(model);
```

## Code

```js
/**
 * Potion Bottle — Codels 3D model library (https://codels.download/models/potion)
 * License: CC0-1.0 · Units: meters · Y-up · Front: +Z · Origin: center of the base (y = 0)
 * Triangles: 600 · Size (W × H × D): 0.20 × 0.28 × 0.20 m
 *
 * Usage:
 *   import { createPotion } from './potion.js';
 *   
 *   const model = createPotion();
 *   model.position.set(0, 0, 0);
 *   scene.add(model);
 *   
 *   // Animations: idle
 *   const mixer = new THREE.AnimationMixer(model);
 *   mixer.clipAction(THREE.AnimationClip.findByName(model.animations, 'idle')).play();
 *   // in your render loop: mixer.update(clock.getDelta());
 */
import * as THREE from 'three';

const deg = THREE.MathUtils.degToRad;

// Revolve a 2D profile [[radius, y], ...] around the Y axis.
function lathe(points, segments, phiStart = 0, phiLength = Math.PI * 2) {
  return new THREE.LatheGeometry(points.map(([x, y]) => new THREE.Vector2(x, y)), segments, phiStart, phiLength);
}

// Build animation clips from simple descriptions (Codels). Play them with THREE.AnimationMixer.
function codelsClips(THREE, entries) {
  var TAU = Math.PI * 2;
  var DEG = Math.PI / 180;
  var AX = { x: new THREE.Vector3(1, 0, 0), y: new THREE.Vector3(0, 1, 0), z: new THREE.Vector3(0, 0, 1) };
  var periodOf = function (a) { return a.type === 'spin' ? 360 / Math.abs(a.speed) : a.period; };
  var clips = [];

  // loops → 'idle'
  var loops = [];
  entries.forEach(function (e) {
    var l = e.anim.filter(function (a) { return a.type !== 'hinge'; });
    if (l.length) loops.push({ target: e.target, anim: l });
  });
  if (loops.length) {
    var periods = [];
    loops.forEach(function (e) { e.anim.forEach(function (a) { periods.push(periodOf(a)); }); });
    var longest = Math.max.apply(null, periods);
    var D = longest;
    var bestErr = Infinity;
    for (var m = 1; m <= 6 && bestErr > 0.01; m++) {
      var err = 0;
      periods.forEach(function (p) {
        var n = Math.max(1, Math.round((longest * m) / p));
        err = Math.max(err, Math.abs((longest * m) / n - p) / p);
      });
      if (err < bestErr - 1e-9) { bestErr = err; D = longest * m; }
    }
    var cycles = function (a) { return Math.max(1, Math.round(D / periodOf(a))); };
    var steps = 24;
    loops.forEach(function (e) { e.anim.forEach(function (a) { steps = Math.max(steps, cycles(a) * 24); }); });
    steps = Math.min(steps, 720);
    var tracks = [];
    loops.forEach(function (e) {
      var o = e.target;
      var q0 = o.quaternion.clone(), p0 = o.position.clone(), s0 = o.scale.clone();
      var q = new THREE.Quaternion(), turn = new THREE.Quaternion(), p = new THREE.Vector3(), s = new THREE.Vector3();
      var has = function (type) { return e.anim.some(function (a) { return a.type === type; }); };
      var turns = has('spin') || has('swing'), moves = has('bob'), grows = has('pulse');
      var times = [], qv = [], pv = [], sv = [];
      for (var k = 0; k <= steps; k++) {
        var t = (D * k) / steps;
        q.copy(q0); p.copy(p0); s.copy(s0);
        e.anim.forEach(function (a) {
          var n = cycles(a);
          var w = Math.sin(TAU * ((n * t) / D + (a.phase || 0)));
          if (a.type === 'spin') q.multiply(turn.setFromAxisAngle(AX[a.axis], (a.speed < 0 ? -TAU : TAU) * ((n * t) / D)));
          else if (a.type === 'swing') q.multiply(turn.setFromAxisAngle(AX[a.axis], a.angle * DEG * w));
          else if (a.type === 'bob') p.addScaledVector(AX[a.axis], a.distance * w);
          else if (a.type === 'pulse') s.multiplyScalar(1 + a.amount * w);
        });
        times.push(t);
        if (turns) qv.push(q.x, q.y, q.z, q.w);
        if (moves) pv.push(p.x, p.y, p.z);
        if (grows) sv.push(s.x, s.y, s.z);
      }
      if (turns) tracks.push(new THREE.QuaternionKeyframeTrack(o.uuid + '.quaternion', times, qv));
      if (moves) tracks.push(new THREE.VectorKeyframeTrack(o.uuid + '.position', times, pv));
      if (grows) tracks.push(new THREE.VectorKeyframeTrack(o.uuid + '.scale', times, sv));
    });
    clips.push(new THREE.AnimationClip('idle', D, tracks));
  }

  // hinge moves → one-shot clips ('open' …)
  var shots = {};
  entries.forEach(function (e) {
    e.anim.forEach(function (a) {
      if (a.type !== 'hinge') return;
      var name = a.clip || 'open';
      (shots[name] = shots[name] || []).push({ target: e.target, a: a });
    });
  });
  Object.keys(shots).forEach(function (name) {
    var tracks = [];
    var length = 0;
    shots[name].forEach(function (h) {
      var o = h.target, q0 = o.quaternion.clone(), q = new THREE.Quaternion(), turn = new THREE.Quaternion();
      var n = Math.max(4, Math.ceil(Math.abs(h.a.angle) / 10));
      var times = [], qv = [];
      for (var k = 0; k <= n; k++) {
        var f = k / n;
        var eased = f * f * (3 - 2 * f);
        q.copy(q0).multiply(turn.setFromAxisAngle(AX[h.a.axis], h.a.angle * DEG * eased));
        times.push(h.a.duration * f);
        qv.push(q.x, q.y, q.z, q.w);
      }
      length = Math.max(length, h.a.duration);
      tracks.push(new THREE.QuaternionKeyframeTrack(o.uuid + '.quaternion', times, qv));
    });
    clips.push(new THREE.AnimationClip(name, length, tracks));
  });
  return clips;
}

export function createPotion() {
  const root = new THREE.Group();
  root.name = 'Potion';

  // Materials
  const glassMat = new THREE.MeshStandardMaterial({ name: 'glass', color: 0xd4ecf4, roughness: 0.08, metalness: 0.1, transparent: true, opacity: 0.35, side: THREE.DoubleSide });
  const liquidMat = new THREE.MeshStandardMaterial({ name: 'liquid', color: 0xe8344e, roughness: 0.15, emissive: 0xe8344e, emissiveIntensity: 0.35 });
  const corkMat = new THREE.MeshStandardMaterial({ name: 'cork', color: 0xb4865a, roughness: 0.95, flatShading: true });
  const twineMat = new THREE.MeshStandardMaterial({ name: 'twine', color: 0x7a5a3a, roughness: 0.95, flatShading: true });
  const paperMat = new THREE.MeshStandardMaterial({ name: 'paper', color: 0xefe2c0, roughness: 0.9, flatShading: true });

  // Parts
  const potion = new THREE.Group();
  potion.name = 'potion';
  root.add(potion);
  const liquid = new THREE.Mesh(lathe([[0, 0], [0.045, 0], [0.078, 0.018], [0.098, 0.06], [0.098, 0.105], [0.0848, 0.1344], [0, 0.1344]], 16), liquidMat);
  liquid.name = 'liquid';
  potion.add(liquid);
  const glass = new THREE.Mesh(lathe([[0.0848, 0.1344], [0.08, 0.145], [0.045, 0.172], [0.03, 0.185], [0.029, 0.24], [0.036, 0.25], [0.028, 0.252], [0.023, 0.23]], 16), glassMat);
  glass.name = 'glass';
  potion.add(glass);
  const cork = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.026, 0.05, 8), corkMat);
  cork.name = 'cork';
  cork.position.set(0, 0.25, 0);
  potion.add(cork);
  const corkTop = new THREE.Mesh(new THREE.CylinderGeometry(0.038, 0.035, 0.008, 8), corkMat);
  corkTop.name = 'corkTop';
  corkTop.position.set(0, 0.279, 0);
  potion.add(corkTop);
  const twine = new THREE.Mesh(new THREE.TorusGeometry(0.0314, 0.0045, 4, 12), twineMat);
  twine.name = 'twine';
  twine.position.set(0, 0.218, 0);
  twine.rotation.set(deg(90), 0, 0);
  potion.add(twine);
  const tagString = new THREE.Mesh(new THREE.BoxGeometry(0.004, 0.03, 0.003), twineMat);
  tagString.name = 'tagString';
  tagString.position.set(0.0246, 0.204, 0.0246);
  tagString.rotation.set(0, deg(45), deg(12));
  potion.add(tagString);
  const tag = new THREE.Mesh(new THREE.BoxGeometry(0.03, 0.04, 0.003), paperMat);
  tag.name = 'tag';
  tag.position.set(0.0306, 0.173, 0.0326);
  tag.rotation.set(deg(-6), deg(45), deg(8));
  potion.add(tag);

  // Animations ('idle') — play them with THREE.AnimationMixer
  root.animations = codelsClips(THREE, [
    { target: potion, anim: [{ type: 'spin', axis: 'y', speed: 45 }] },
  ]);

  root.traverse((o) => {
    if (o.isMesh) {
      o.castShadow = true;
      o.receiveShadow = true;
    }
  });
  return root;
}

export default createPotion;
```

---
Codels — 3D models as code for three.js. Search more: https://codels.download/api/v1/models?q=<query> · Guide: https://codels.download/llms.txt
