sandeep85's picture
Add OptiQ 4-bit quant of ccore/Qwen3.5-4B-3D-Coder
4d50ce8 verified
Raw History Blame Contribute Delete
17.3 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Three Potion Bottles</title>
<style>
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #dfe5df;
}
#stage {
position: fixed;
inset: 0;
}
canvas {
display: block;
width: 100%;
height: 100%;
touch-action: none;
cursor: grab;
outline: none;
}
canvas.dragging {
cursor: grabbing;
}
.hud {
position: fixed;
top: 24px;
left: 26px;
z-index: 2;
max-width: min(360px, calc(100vw - 52px));
padding: 16px 18px;
border: 1px solid rgba(255, 255, 255, 0.58);
border-radius: 16px;
background: rgba(250, 248, 240, 0.72);
box-shadow: 0 12px 36px rgba(49, 58, 51, 0.12);
backdrop-filter: blur(12px);
pointer-events: none;
}
.eyebrow {
margin-bottom: 5px;
color: #718078;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.18em;
text-transform: uppercase;
}
h1 {
margin: 0;
color: #303b36;
font-size: clamp(25px, 4vw, 38px);
font-weight: 750;
letter-spacing: -0.045em;
}
.hud p {
margin: 8px 0 0;
color: #69746d;
font-size: 13px;
}
.controls {
position: fixed;
right: 22px;
bottom: 22px;
z-index: 3;
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border: 1px solid rgba(255, 255, 255, 0.62);
border-radius: 12px;
background: rgba(250, 248, 240, 0.76);
box-shadow: 0 10px 30px rgba(49, 58, 51, 0.12);
backdrop-filter: blur(10px);
}
.status {
color: #738078;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
button {
border: 0;
border-radius: 8px;
padding: 9px 12px;
background: #415047;
color: #fffdf5;
font: inherit;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: transform 150ms ease, background 150ms ease;
}
button:hover {
background: #526157;
transform: translateY(-1px);
}
button:active {
transform: translateY(0);
}
@media (max-width: 600px) {
.hud {
top: 14px;
left: 14px;
padding: 13px 15px;
}
.hud p {
max-width: 230px;
font-size: 12px;
}
.controls {
right: 14px;
bottom: 14px;
}
.status {
display: none;
}
}
</style>
</head>
<body>
<main id="stage" aria-label="Interactive three-dimensional potion bottle scene"></main>
<section class="hud">
<div class="eyebrow">Interactive 3D asset</div>
<h1>Three Potions</h1>
<p>Drag to orbit · Scroll or pinch to zoom · Double-click to reset</p>
</section>
<div class="controls">
<span class="status" id="status">Auto spin</span>
<button id="pause" type="button">Pause rotation</button>
</div>
<script type="module">
import * as THREE from "https://cdnjs.cloudflare.com/ajax/libs/three.js/0.160.0/three.module.min.js";
const stage = document.querySelector("#stage");
const pauseButton = document.querySelector("#pause");
const status = document.querySelector("#status");
// ---------------------------------------------------------------------
// Renderer, scene, and camera
// ---------------------------------------------------------------------
const renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: "high-performance"
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.08;
renderer.domElement.setAttribute(
"aria-label",
"Three colored potion bottles with corks resting on a wooden tray"
);
stage.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xdfe5df);
scene.fog = new THREE.Fog(0xdfe5df, 13, 30);
const camera = new THREE.PerspectiveCamera(
38,
window.innerWidth / window.innerHeight,
0.1,
100
);
const controls = {
yaw: 0.58,
pitch: 0.52,
distance: 8.4,
target: new THREE.Vector3(0, 1.08, 0)
};
function updateCamera() {
const horizontalDistance =
controls.distance * Math.cos(controls.pitch);
camera.position.set(
controls.target.x + Math.sin(controls.yaw) * horizontalDistance,
controls.target.y + Math.sin(controls.pitch) * controls.distance,
controls.target.z + Math.cos(controls.yaw) * horizontalDistance
);
camera.lookAt(controls.target);
}
updateCamera();
// ---------------------------------------------------------------------
// Lighting
// ---------------------------------------------------------------------
scene.add(new THREE.HemisphereLight(0xf8fcff, 0x8a6848, 2.1));
const keyLight = new THREE.DirectionalLight(0xfff1d7, 3.2);
keyLight.position.set(-4.5, 8, 5.5);
keyLight.castShadow = true;
keyLight.shadow.mapSize.set(2048, 2048);
keyLight.shadow.camera.left = -7;
keyLight.shadow.camera.right = 7;
keyLight.shadow.camera.top = 7;
keyLight.shadow.camera.bottom = -7;
keyLight.shadow.camera.near = 0.5;
keyLight.shadow.camera.far = 20;
keyLight.shadow.normalBias = 0.025;
keyLight.target.position.set(0, 0.8, 0);
scene.add(keyLight, keyLight.target);
const fillLight = new THREE.DirectionalLight(0xc5dcff, 1.25);
fillLight.position.set(5, 3.5, -4);
scene.add(fillLight);
// ---------------------------------------------------------------------
// Materials and geometry helpers
// ---------------------------------------------------------------------
function createWoodTexture() {
const canvas = document.createElement("canvas");
canvas.width = 512;
canvas.height = 512;
const context = canvas.getContext("2d");
context.fillStyle = "#8a542f";
context.fillRect(0, 0, 512, 512);
let seed = 9137;
function random() {
seed = (seed * 16807) % 2147483647;
return (seed - 1) / 2147483646;
}
for (let i = 0; i < 100; i++) {
const y = random() * 512;
const amplitude = 1 + random() * 5;
const frequency = 0.008 + random() * 0.018;
context.beginPath();
context.moveTo(0, y);
for (let x = 0; x <= 512; x += 8) {
const wave = Math.sin(x * frequency + i) * amplitude;
const noise = (random() - 0.5) * 2;
context.lineTo(x, y + wave + noise);
}
context.strokeStyle = `rgba(48, 23, 10, ${0.08 + random() * 0.16})`;
context.lineWidth = 0.5 + random() * 2.2;
context.stroke();
}
for (let i = 0; i < 8; i++) {
const x = random() * 512;
const y = random() * 512;
const radiusX = 8 + random() * 24;
const radiusY = 2 + random() * 7;
context.beginPath();
context.ellipse(x, y, radiusX, radiusY, 0, 0, Math.PI * 2);
context.strokeStyle = "rgba(45, 22, 9, 0.18)";
context.lineWidth = 2;
context.stroke();
}
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(1.5, 1.5);
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
return texture;
}
const woodTexture = createWoodTexture();
const materials = {
wood: new THREE.MeshStandardMaterial({
map: woodTexture,
color: 0xffffff,
roughness: 0.78
}),
darkWood: new THREE.MeshStandardMaterial({
map: woodTexture,
color: 0x6d4127,
roughness: 0.86
}),
cork: new THREE.MeshStandardMaterial({
color: 0xb9864e,
roughness: 0.82
}),
corkTop: new THREE.MeshStandardMaterial({
color: 0xd0a264,
roughness: 0.72
}),
label: new THREE.MeshStandardMaterial({
color: 0xf3e3bd,
roughness: 0.88
})
};
function addMesh(
geometry,
material,
parent,
name,
position = [0, 0, 0],
rotation = [0, 0, 0],
castShadow = true,
receiveShadow = true
) {
const object = new THREE.Mesh(geometry, material);
object.name = name;
object.position.set(...position);
object.rotation.set(...rotation);
object.castShadow = castShadow;
object.receiveShadow = receiveShadow;
parent.add(object);
return object;
}
// ---------------------------------------------------------------------
// Ground plane
// ---------------------------------------------------------------------
const ground = addMesh(
new THREE.PlaneGeometry(40, 40),
new THREE.MeshStandardMaterial({
color: 0xd9dfda,
roughness: 1
}),
scene,
"Plain ground plane",
[0, -0.012, 0],
[-Math.PI / 2, 0, 0],
false,
true
);
// ---------------------------------------------------------------------
// Turntable and wooden tray
// ---------------------------------------------------------------------
const turntable = new THREE.Group();
turntable.name = "Rotating turntable";
scene.add(turntable);
addMesh(
new THREE.CylinderGeometry(3.08, 3.14, 0.1, 96),
materials.darkWood,
turntable,
"Turntable plinth",
[0, -0.04, 0]
);
addMesh(
new THREE.TorusGeometry(3.08, 0.025, 10, 96),
materials.wood,
turntable,
"Turntable rim",
[0, 0.012, 0],
[Math.PI / 2, 0, 0]
);
const tray = new THREE.Group();
tray.name = "Small wooden tray";
tray.position.y = 0.07;
turntable.add(tray);
addMesh(
new THREE.BoxGeometry(4.95, 0.16, 3.3),
materials.wood,
tray,
"Tray base"
);
addMesh(
new THREE.BoxGeometry(0.16, 0.36, 3.3),
materials.wood,
tray,
"Left tray rail",
[-2.39, 0, 0]
);
addMesh(
new THREE.BoxGeometry(0.16, 0.36, 3.3),
materials.wood,
tray,
"Right tray rail",
[2.39, 0, 0]
);
addMesh(
new THREE.BoxGeometry(4.83, 0.36, 0.16),
materials.wood,
tray,
"Front tray rail",
[0, 0, 1.59]
);
addMesh(
new THREE.BoxGeometry(4.83, 0.36, 0.16),
materials.wood,
tray,
"Back tray rail",
[0, 0, -1.59]
);
addMesh(
new THREE.BoxGeometry(4.68, 0.035, 3.08),
materials.wood,
tray,
"Tray floor"
);
// ---------------------------------------------------------------------
// Bottle construction
// ---------------------------------------------------------------------
const bottleProfile = [
new THREE.Vector2(0.0, 0.0),
new THREE.Vector2(0.23, 0.0),
new THREE.Vector2(0.33, 0.08),
new THREE.Vector2(0.41, 0.22),
new THREE.Vector2(0.43, 0.55),
new THREE.Vector2(0.43, 0.95),
new THREE.Vector2(0.4, 1.2),
new THREE.Vector2(0.31, 1.4),
new THREE.Vector2(0.22, 1.55),
new THREE.Vector2(0.17, 1.68),
new THREE.Vector2(0.17, 1.82)
];
function createLabelTexture() {
const canvas = document.createElement("canvas");
canvas.width = 256;
canvas.height = 128;
const context = canvas.getContext("2d");
context.fillStyle = "#f3e3bd";
context.fillRect(0, 0, 256, 128);
context.strokeStyle = "#b78b45";
context.lineWidth = 8;
context.strokeRect(9, 9, 238, 110);
context.fillStyle = "#59452d";
context.textAlign = "center";
context.textBaseline = "middle";
context.font = "700 42px Georgia, serif";
context.fillText("POTION", 128, 65);
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
return texture;
}
const labelTexture = createLabelTexture();
function createBottle(name, bodyColor, xPosition) {
const bottle = new THREE.Group();
bottle.name = name;
bottle.position.set(xPosition, 0.17, 0);
turntable.add(bottle);
const bodyMaterial = new THREE.MeshPhysicalMaterial({
color: bodyColor,
roughness: 0.22,
metalness: 0,
clearcoat: 0.65,
clearcoatRoughness: 0.24
});
addMesh(
new THREE.LatheGeometry(bottleProfile, 64),
bodyMaterial,
bottle,
`${name} bottle body`
);
addMesh(
new THREE.CylinderGeometry(0.17, 0.19, 0.42, 32),
materials.cork,
bottle,
`${name} cork`
);
addMesh(
new THREE.CylinderGeometry(0.172, 0.172, 0.025, 32),
materials.corkTop,
bottle,
`${name} cork top`
);
const label = addMesh(
new THREE.PlaneGeometry(0.48, 0.24),
new THREE.MeshStandardMaterial({
map: labelTexture,
roughness: 0.88
}),
bottle,
`${name} front label`,
[0, 0.77, 0.425]
);
label.castShadow = false;
return bottle;
}
createBottle("Crimson potion bottle", 0xc6353b, -1.25);
createBottle("Cobalt potion bottle", 0x245ad4, 0);
createBottle("Emerald potion bottle", 0x2d8d65, 1.25);
// ---------------------------------------------------------------------
// Direct mouse orbit controls — no external control modules
// ---------------------------------------------------------------------
let dragging = false;
let previousX = 0;
let previousY = 0;
const canvas = renderer.domElement;
canvas.addEventListener("pointerdown", (event) => {
if (event.button !== 0) return;
dragging = true;
previousX = event.clientX;
previousY = event.clientY;
canvas.setPointerCapture(event.pointerId);
canvas.classList.add("dragging");
});
canvas.addEventListener("pointermove", (event) => {
if (!dragging) return;
const deltaX = event.clientX - previousX;
const deltaY = event.clientY - previousY;
previousX = event.clientX;
previousY = event.clientY;
controls.yaw -= deltaX * 0.006;
controls.pitch = THREE.MathUtils.clamp(
controls.pitch + deltaY * 0.005,
0.25,
1.35
);
updateCamera();
});
function endDrag(event) {
dragging = false;
canvas.classList.remove("dragging");
if (event.pointerId !== undefined && canvas.hasPointerCapture(event.pointerId)) {
canvas.releasePointerCapture(event.pointerId);
}
}
canvas.addEventListener("pointerup", endDrag);
canvas.addEventListener("pointercancel", endDrag);
canvas.addEventListener(
"wheel",
(event) => {
event.preventDefault();
controls.distance = THREE.MathUtils.clamp(
controls.distance * Math.exp(event.deltaY * 0.001),
5.6,
13
);
updateCamera();
},
{ passive: false }
);
canvas.addEventListener("dblclick", () => {
controls.yaw = 0.58;
controls.pitch = 0.52;
controls.distance = 8.4;
updateCamera();
});
canvas.addEventListener("contextmenu", (event) => {
event.preventDefault();
});
// ---------------------------------------------------------------------
// Animation and UI
// ---------------------------------------------------------------------
let elapsed = 0;
let paused = false;
pauseButton.addEventListener("click", () => {
paused = !paused;
pauseButton.textContent = paused ? "Resume rotation" : "Pause rotation";
status.textContent = paused ? "Paused" : "Auto spin";
});
const clock = new THREE.Clock();
function animate() {
const delta = Math.min(clock.getDelta(), 0.05);
if (!paused) {
elapsed += delta;
}
turntable.rotation.y = elapsed * 0.28;
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>