94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
import * as THREE from 'three'
|
|
|
|
export const cardDimensions = {
|
|
width: 0.063,
|
|
height: 0.0882,
|
|
thickness: 0.0004,
|
|
cornerRadius: 0.0028,
|
|
} as const
|
|
|
|
const sceneHeight = 4
|
|
const sceneScale = sceneHeight / cardDimensions.height
|
|
|
|
export const cardSceneDimensions = {
|
|
width: cardDimensions.width * sceneScale,
|
|
height: cardDimensions.height * sceneScale,
|
|
thickness: cardDimensions.thickness * sceneScale,
|
|
cornerRadius: cardDimensions.cornerRadius * sceneScale,
|
|
} as const
|
|
|
|
function createRoundedRectangle(width: number, height: number, radius: number) {
|
|
const halfWidth = width / 2
|
|
const halfHeight = height / 2
|
|
const shape = new THREE.Shape()
|
|
|
|
shape.moveTo(-halfWidth + radius, -halfHeight)
|
|
shape.lineTo(halfWidth - radius, -halfHeight)
|
|
shape.quadraticCurveTo(halfWidth, -halfHeight, halfWidth, -halfHeight + radius)
|
|
shape.lineTo(halfWidth, halfHeight - radius)
|
|
shape.quadraticCurveTo(halfWidth, halfHeight, halfWidth - radius, halfHeight)
|
|
shape.lineTo(-halfWidth + radius, halfHeight)
|
|
shape.quadraticCurveTo(-halfWidth, halfHeight, -halfWidth, halfHeight - radius)
|
|
shape.lineTo(-halfWidth, -halfHeight + radius)
|
|
shape.quadraticCurveTo(-halfWidth, -halfHeight, -halfWidth + radius, -halfHeight)
|
|
|
|
return shape
|
|
}
|
|
|
|
export function createCardGeometry(
|
|
frontMaterial: THREE.Material,
|
|
backMaterial: THREE.Material,
|
|
edgeMaterial: THREE.Material,
|
|
) {
|
|
const { width, height, thickness, cornerRadius } = cardSceneDimensions
|
|
const shape = createRoundedRectangle(width, height, cornerRadius)
|
|
|
|
const bodyGeometry = new THREE.ExtrudeGeometry(shape, {
|
|
curveSegments: 16,
|
|
depth: thickness,
|
|
steps: 1,
|
|
bevelEnabled: false,
|
|
})
|
|
bodyGeometry.translate(0, 0, -thickness / 2)
|
|
// Extrusion caps would duplicate the dedicated faces and z-fight at Pack's camera distances.
|
|
const sideWalls = bodyGeometry.groups.find((group) => group.materialIndex === 1)
|
|
if (!sideWalls) throw new Error('Card extrusion is missing its side-wall geometry')
|
|
bodyGeometry.setDrawRange(sideWalls.start, sideWalls.count)
|
|
|
|
const faceGeometry = new THREE.ShapeGeometry(shape, 16)
|
|
const positions = faceGeometry.getAttribute('position')
|
|
const uvs = faceGeometry.getAttribute('uv')
|
|
for (let index = 0; index < positions.count; index += 1) {
|
|
uvs.setXY(
|
|
index,
|
|
positions.getX(index) / width + 0.5,
|
|
positions.getY(index) / height + 0.5,
|
|
)
|
|
}
|
|
uvs.needsUpdate = true
|
|
const faceOffset = thickness / 2
|
|
|
|
const body = new THREE.Mesh(bodyGeometry, edgeMaterial)
|
|
body.name = 'CARD_EDGE'
|
|
|
|
const front = new THREE.Mesh(faceGeometry, frontMaterial)
|
|
front.name = 'CARD_FRONT'
|
|
front.position.z = faceOffset
|
|
|
|
const back = new THREE.Mesh(faceGeometry.clone(), backMaterial)
|
|
back.name = 'CARD_BACK'
|
|
back.position.z = -faceOffset
|
|
back.rotation.y = Math.PI
|
|
|
|
const card = new THREE.Group()
|
|
card.name = 'CARD_PROCEDURAL'
|
|
card.add(body, front, back)
|
|
|
|
for (const mesh of [body, front, back]) {
|
|
mesh.castShadow = true
|
|
mesh.receiveShadow = true
|
|
}
|
|
|
|
return card
|
|
}
|