Files
homarr/tools/motion-reel/three/logo-rig.ts
T
+12 fcd7c979df feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
2026-10-02 21:00:14 +02:00

187 lines
6.8 KiB
TypeScript

// Extracted verbatim from origin/feat/3d-logo-lab (homarr-3d-lab/homarr-scene.tsx):
// the lobster extrusion + skinned claw/antenna rig, minus the React/OrbitControls shell.
import * as THREE from "three";
import { SVGLoader } from "three/examples/jsm/loaders/SVGLoader.js";
import type { LabConfig } from "./lab-types";
const materialSettings = {
red: { color: 0xfa5352, metalness: 0.12, roughness: 0.28, clearcoat: 0.7 },
matte: { color: 0xfa5352, metalness: 0, roughness: 0.82, clearcoat: 0 },
glossy: { color: 0xfa5352, metalness: 0.05, roughness: 0.1, clearcoat: 1 },
metal: { color: 0xfa5352, metalness: 0.72, roughness: 0.23, clearcoat: 0.8 },
chrome: { color: 0xd9dce2, metalness: 1, roughness: 0.12, clearcoat: 1 },
plastic: { color: 0xfa5352, metalness: 0, roughness: 0.38, clearcoat: 0.45 },
glass: { color: 0xfa5352, metalness: 0.05, roughness: 0.08, clearcoat: 1, transparent: true, opacity: 0.54 },
} as const;
type Side = -1 | 1;
type JointKind = "claw" | "antenna";
interface JointChain {
mesh: THREE.SkinnedMesh;
shoulder: THREE.Bone;
middle: THREE.Bone;
tip: THREE.Bone;
side: Side;
}
interface LobsterRig {
claws: JointChain[];
antennae: JointChain[];
eyes: THREE.Mesh[];
}
// The original SVG has one path with seven separate silhouettes in this order.
const lobsterParts = [
"left-antenna",
"right-antenna",
"body",
"left-eye",
"right-eye",
"right-claw",
"left-claw",
] as const;
function smoothstep(start: number, end: number, value: number) {
const t = THREE.MathUtils.clamp((value - start) / (end - start), 0, 1);
return t * t * (3 - 2 * t);
}
function makeJointedPart(
geometry: THREE.ExtrudeGeometry,
material: THREE.Material,
center: THREE.Vector3,
kind: JointKind,
side: Side,
): JointChain {
const mesh = new THREE.SkinnedMesh(geometry, material);
mesh.frustumCulled = false;
// Coordinates here are from /logo/homarr.svg. The shoulder stays fixed;
// the other two bones follow the silhouette's center line.
const joints: [[number, number], [number, number], [number, number]] =
kind === "claw"
? [
[134, 335],
[95, 270],
[65, 180],
]
: [
[227, 226],
[185, 165],
[169, 105],
];
const at = ([x, y]: [number, number]) => new THREE.Vector3((side === -1 ? x : 512 - x) - center.x, y - center.y, 0);
const [shoulderJoint, middleJoint, tipJoint] = joints;
const shoulderPosition = at(shoulderJoint);
const middlePosition = at(middleJoint);
const tipPosition = at(tipJoint);
const shoulder = new THREE.Bone();
shoulder.name = `${kind}-${side}-shoulder`;
shoulder.position.copy(shoulderPosition);
const middle = new THREE.Bone();
middle.name = `${kind}-${side}-middle`;
middle.position.copy(middlePosition).sub(shoulderPosition);
const tip = new THREE.Bone();
tip.name = `${kind}-${side}-tip`;
tip.position.copy(tipPosition).sub(middlePosition);
shoulder.add(middle);
middle.add(tip);
mesh.add(shoulder);
const positions = geometry.getAttribute("position");
const skinIndices: number[] = [];
const skinWeights: number[] = [];
const baseY = kind === "claw" ? 370 : 230;
const topY = kind === "claw" ? 113 : 83;
for (let index = 0; index < positions.count; index++) {
const progress = THREE.MathUtils.clamp((baseY - (center.y + positions.getY(index))) / (baseY - topY), 0, 1);
const middleWeight = smoothstep(0.08, 0.48, progress);
const tipWeight = smoothstep(0.42, 0.9, progress);
skinIndices.push(0, 1, 2, 0);
skinWeights.push(1 - middleWeight, middleWeight * (1 - tipWeight), middleWeight * tipWeight, 0);
}
geometry.setAttribute("skinIndex", new THREE.Uint16BufferAttribute(skinIndices, 4));
geometry.setAttribute("skinWeight", new THREE.Float32BufferAttribute(skinWeights, 4));
mesh.bind(new THREE.Skeleton([shoulder, middle, tip]));
return { mesh, shoulder, middle, tip, side };
}
function makeMaterial(config: LabConfig) {
const settings = materialSettings[config.material];
return new THREE.MeshPhysicalMaterial({
...settings,
wireframe: config.wireframe,
side: THREE.DoubleSide,
});
}
function styleForVariant(config: LabConfig) {
if (config.variant === "chunky") return { depth: config.depth * 1.9, bevel: config.bevel * 1.45, scaleZ: 1 };
if (config.variant === "rounded")
return { depth: config.depth * 1.2, bevel: Math.max(0.13, config.bevel * 1.7), scaleZ: 1.22 };
if (config.variant === "full")
return { depth: config.depth * 1.7, bevel: Math.max(0.12, config.bevel * 1.5), scaleZ: 1.55 };
return { depth: config.depth, bevel: config.bevel, scaleZ: 1 };
}
function extrudeSvg(svg: string, config: LabConfig) {
const parsed = new SVGLoader().parse(svg);
const model = new THREE.Group();
model.name = "CenteredLobsterGeometry";
const rig: LobsterRig = { claws: [], antennae: [], eyes: [] };
const style = styleForVariant(config);
let meshIndex = 0;
parsed.paths.forEach((path) => {
SVGLoader.createShapes(path).forEach((shape) => {
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: style.depth * 54,
bevelEnabled: true,
bevelThickness: style.bevel * 20,
bevelSize: style.bevel * 15,
bevelSegments: config.variant === "metal" ? 3 : 5,
curveSegments: config.variant === "full" ? 18 : 12,
});
geometry.computeVertexNormals();
geometry.computeBoundingBox();
const partCenter = geometry.boundingBox?.getCenter(new THREE.Vector3()) ?? new THREE.Vector3();
geometry.translate(-partCenter.x, -partCenter.y, -partCenter.z);
const part = lobsterParts[meshIndex];
const side: Side = part?.startsWith("left") ? -1 : 1;
let mesh: THREE.Mesh;
if (part?.endsWith("claw") || part?.endsWith("antenna")) {
const kind = part.endsWith("claw") ? "claw" : "antenna";
const chain = makeJointedPart(geometry, makeMaterial(config), partCenter, kind, side);
mesh = chain.mesh;
rig[kind === "claw" ? "claws" : "antennae"].push(chain);
} else {
mesh = new THREE.Mesh(geometry, makeMaterial(config));
if (part?.endsWith("eye")) rig.eyes.push(mesh);
}
mesh.position.copy(partCenter);
mesh.name = part ?? `badge-layer-${meshIndex}`;
meshIndex++;
mesh.castShadow = true;
mesh.receiveShadow = true;
model.add(mesh);
});
});
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
model.children.forEach((child) => {
child.position.sub(center);
child.userData.home = child.position.clone();
});
const centeredBox = new THREE.Box3().setFromObject(model);
const size = centeredBox.getSize(new THREE.Vector3());
const scale = 5.15 / Math.max(size.x, size.y);
model.scale.set(scale, -scale, scale * style.scaleZ);
return { model, rig };
}
export { extrudeSvg, makeMaterial };
export type { LobsterRig };