From 603c7d2df1c77cfe754ff0e6001051fc979d8426 Mon Sep 17 00:00:00 2001 From: EnderRomantice Date: Sun, 7 Jun 2026 19:00:14 +0800 Subject: [PATCH 01/12] feat(Lanyard): render card front/back from separate images (JS/CSS) Replace the single `cardImage` prop with `frontImage` and `backImage`, plus an `imageFit` ('cover' | 'contain') option. The card model's atlas maps the front face to the left half and the back face to the right half, so each image is composited into its own half of a canvas texture and the two faces render independently. Images are drawn aspect-preserving (no stretching). When no image is supplied, the original baked texture is returned unchanged, so default rendering is untouched. --- src/content/Components/Lanyard/Lanyard.jsx | 98 ++++++++++++++++++++-- 1 file changed, 92 insertions(+), 6 deletions(-) diff --git a/src/content/Components/Lanyard/Lanyard.jsx b/src/content/Components/Lanyard/Lanyard.jsx index 4c010ede4..ae3c4dfa8 100644 --- a/src/content/Components/Lanyard/Lanyard.jsx +++ b/src/content/Components/Lanyard/Lanyard.jsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-unknown-property */ 'use client'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { Canvas, extend, useFrame } from '@react-three/fiber'; import { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei'; import { BallCollider, CuboidCollider, Physics, RigidBody, useRopeJoint, useSphericalJoint } from '@react-three/rapier'; @@ -15,7 +15,28 @@ import './Lanyard.css'; extend({ MeshLineGeometry, MeshLineMaterial }); -export default function Lanyard({ position = [0, 0, 30], gravity = [0, -40, 0], fov = 20, transparent = true }) { +// 1x1 transparent pixel — lets useTexture be called unconditionally when a +// front/back image isn't supplied. +const BLANK_PIXEL = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; + +// The card model's front face is UV-mapped to the LEFT half of the texture +// atlas and the back face to the RIGHT half (measured from card.glb). Each +// custom image is composited into its own half so the two faces render +// independently, aspect-preserving (no stretching). +const FRONT_UV_RECT = { x: 0, y: 0, w: 0.5, h: 0.755 }; +const BACK_UV_RECT = { x: 0.5, y: 0, w: 0.5, h: 0.757 }; + +export default function Lanyard({ + position = [0, 0, 30], + gravity = [0, -40, 0], + fov = 20, + transparent = true, + frontImage = null, + backImage = null, + imageFit = 'cover', + lanyardImage = null +}) { const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768); useEffect(() => { @@ -34,7 +55,13 @@ export default function Lanyard({ position = [0, 0, 30], gravity = [0, -40, 0], > - + ); } -function Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }) { +function Band({ + maxSpeed = 50, + minSpeed = 0, + isMobile = false, + frontImage = null, + backImage = null, + imageFit = 'cover', + lanyardImage = null +}) { const band = useRef(), fixed = useRef(), j1 = useRef(), @@ -83,7 +118,58 @@ function Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }) { dir = new THREE.Vector3(); const segmentProps = { type: 'dynamic', canSleep: true, colliders: false, angularDamping: 4, linearDamping: 4 }; const { nodes, materials } = useGLTF(cardGLB); - const texture = useTexture(lanyard); + const texture = useTexture(lanyardImage || lanyard); + // useTexture must be called unconditionally; use a blank pixel when an image + // isn't supplied for a given face, then skip compositing it below. + const frontTex = useTexture(frontImage || BLANK_PIXEL); + const backTex = useTexture(backImage || BLANK_PIXEL); + + // Composite the front/back images into the card's texture atlas (front = left + // half, back = right half). Each image is drawn aspect-preserving (no stretch). + const cardMap = useMemo(() => { + const baseMap = materials.base.map; + if (!frontImage && !backImage) return baseMap; + + const baseImg = baseMap.image; + const W = baseImg.width; + const H = baseImg.height; + const canvas = document.createElement('canvas'); + canvas.width = W; + canvas.height = H; + const ctx = canvas.getContext('2d'); + if (!ctx) return baseMap; + // Keep the original baked atlas for the card edges and any untouched face. + ctx.drawImage(baseImg, 0, 0, W, H); + + const drawFitted = (img, rect) => { + const rx = rect.x * W; + const ry = rect.y * H; + const rw = rect.w * W; + const rh = rect.h * H; + const pick = imageFit === 'contain' ? Math.min : Math.max; + const scale = pick(rw / img.width, rh / img.height); + const dw = img.width * scale; + const dh = img.height * scale; + const dx = rx + (rw - dw) / 2; + const dy = ry + (rh - dh) / 2; + ctx.save(); + ctx.beginPath(); + ctx.rect(rx, ry, rw, rh); + ctx.clip(); + ctx.drawImage(img, dx, dy, dw, dh); + ctx.restore(); + }; + + if (frontImage && frontTex.image) drawFitted(frontTex.image, FRONT_UV_RECT); + if (backImage && backTex.image) drawFitted(backTex.image, BACK_UV_RECT); + + const composite = new THREE.CanvasTexture(canvas); + composite.colorSpace = THREE.SRGBColorSpace; + composite.flipY = baseMap.flipY; + composite.anisotropy = 16; + composite.needsUpdate = true; + return composite; + }, [frontImage, backImage, imageFit, frontTex, backTex, materials.base.map]); const [curve] = useState( () => new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]) @@ -165,7 +251,7 @@ function Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }) { > Date: Sun, 7 Jun 2026 19:00:14 +0800 Subject: [PATCH 02/12] feat(Lanyard): render card front/back from separate images (JS/Tailwind) Mirror the front/back compositing API (frontImage, backImage, imageFit) in the JavaScript + Tailwind variant. Default rendering is preserved when no custom image is provided. --- src/tailwind/Components/Lanyard/Lanyard.jsx | 98 +++++++++++++++++++-- 1 file changed, 92 insertions(+), 6 deletions(-) diff --git a/src/tailwind/Components/Lanyard/Lanyard.jsx b/src/tailwind/Components/Lanyard/Lanyard.jsx index fdb62506b..09995a315 100644 --- a/src/tailwind/Components/Lanyard/Lanyard.jsx +++ b/src/tailwind/Components/Lanyard/Lanyard.jsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-unknown-property */ 'use client'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { Canvas, extend, useFrame } from '@react-three/fiber'; import { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei'; import { BallCollider, CuboidCollider, Physics, RigidBody, useRopeJoint, useSphericalJoint } from '@react-three/rapier'; @@ -14,7 +14,28 @@ import * as THREE from 'three'; extend({ MeshLineGeometry, MeshLineMaterial }); -export default function Lanyard({ position = [0, 0, 30], gravity = [0, -40, 0], fov = 20, transparent = true }) { +// 1x1 transparent pixel — lets useTexture be called unconditionally when a +// front/back image isn't supplied. +const BLANK_PIXEL = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; + +// The card model's front face is UV-mapped to the LEFT half of the texture +// atlas and the back face to the RIGHT half (measured from card.glb). Each +// custom image is composited into its own half so the two faces render +// independently, aspect-preserving (no stretching). +const FRONT_UV_RECT = { x: 0, y: 0, w: 0.5, h: 0.755 }; +const BACK_UV_RECT = { x: 0.5, y: 0, w: 0.5, h: 0.757 }; + +export default function Lanyard({ + position = [0, 0, 30], + gravity = [0, -40, 0], + fov = 20, + transparent = true, + frontImage = null, + backImage = null, + imageFit = 'cover', + lanyardImage = null +}) { const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768); useEffect(() => { @@ -33,7 +54,13 @@ export default function Lanyard({ position = [0, 0, 30], gravity = [0, -40, 0], > - + ); } -function Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }) { +function Band({ + maxSpeed = 50, + minSpeed = 0, + isMobile = false, + frontImage = null, + backImage = null, + imageFit = 'cover', + lanyardImage = null +}) { const band = useRef(), fixed = useRef(), j1 = useRef(), @@ -82,7 +117,58 @@ function Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }) { dir = new THREE.Vector3(); const segmentProps = { type: 'dynamic', canSleep: true, colliders: false, angularDamping: 4, linearDamping: 4 }; const { nodes, materials } = useGLTF(cardGLB); - const texture = useTexture(lanyard); + const texture = useTexture(lanyardImage || lanyard); + // useTexture must be called unconditionally; use a blank pixel when an image + // isn't supplied for a given face, then skip compositing it below. + const frontTex = useTexture(frontImage || BLANK_PIXEL); + const backTex = useTexture(backImage || BLANK_PIXEL); + + // Composite the front/back images into the card's texture atlas (front = left + // half, back = right half). Each image is drawn aspect-preserving (no stretch). + const cardMap = useMemo(() => { + const baseMap = materials.base.map; + if (!frontImage && !backImage) return baseMap; + + const baseImg = baseMap.image; + const W = baseImg.width; + const H = baseImg.height; + const canvas = document.createElement('canvas'); + canvas.width = W; + canvas.height = H; + const ctx = canvas.getContext('2d'); + if (!ctx) return baseMap; + // Keep the original baked atlas for the card edges and any untouched face. + ctx.drawImage(baseImg, 0, 0, W, H); + + const drawFitted = (img, rect) => { + const rx = rect.x * W; + const ry = rect.y * H; + const rw = rect.w * W; + const rh = rect.h * H; + const pick = imageFit === 'contain' ? Math.min : Math.max; + const scale = pick(rw / img.width, rh / img.height); + const dw = img.width * scale; + const dh = img.height * scale; + const dx = rx + (rw - dw) / 2; + const dy = ry + (rh - dh) / 2; + ctx.save(); + ctx.beginPath(); + ctx.rect(rx, ry, rw, rh); + ctx.clip(); + ctx.drawImage(img, dx, dy, dw, dh); + ctx.restore(); + }; + + if (frontImage && frontTex.image) drawFitted(frontTex.image, FRONT_UV_RECT); + if (backImage && backTex.image) drawFitted(backTex.image, BACK_UV_RECT); + + const composite = new THREE.CanvasTexture(canvas); + composite.colorSpace = THREE.SRGBColorSpace; + composite.flipY = baseMap.flipY; + composite.anisotropy = 16; + composite.needsUpdate = true; + return composite; + }, [frontImage, backImage, imageFit, frontTex, backTex, materials.base.map]); const [curve] = useState( () => new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]) @@ -164,7 +250,7 @@ function Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }) { > Date: Sun, 7 Jun 2026 19:00:14 +0800 Subject: [PATCH 03/12] feat(Lanyard): render card front/back from separate images (TS/CSS) Mirror the front/back compositing API (frontImage, backImage, imageFit) in the TypeScript + CSS variant, with prop types on LanyardProps and BandProps. Default rendering is preserved when no custom image is provided. --- src/ts-default/Components/Lanyard/Lanyard.tsx | 101 ++++++++++++++++-- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/src/ts-default/Components/Lanyard/Lanyard.tsx b/src/ts-default/Components/Lanyard/Lanyard.tsx index 661c015b8..e4f239ab7 100644 --- a/src/ts-default/Components/Lanyard/Lanyard.tsx +++ b/src/ts-default/Components/Lanyard/Lanyard.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-unknown-property */ 'use client'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { Canvas, extend, useFrame } from '@react-three/fiber'; import { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei'; import { @@ -23,18 +23,38 @@ import './Lanyard.css'; extend({ MeshLineGeometry, MeshLineMaterial }); +// 1x1 transparent pixel — lets useTexture be called unconditionally when a +// front/back image isn't supplied. +const BLANK_PIXEL = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; + +// The card model's front face is UV-mapped to the LEFT half of the texture +// atlas and the back face to the RIGHT half (measured from card.glb). Each +// custom image is composited into its own half so the two faces render +// independently, aspect-preserving (no stretching). +const FRONT_UV_RECT = { x: 0, y: 0, w: 0.5, h: 0.755 }; +const BACK_UV_RECT = { x: 0.5, y: 0, w: 0.5, h: 0.757 }; + interface LanyardProps { position?: [number, number, number]; gravity?: [number, number, number]; fov?: number; transparent?: boolean; + frontImage?: string | null; + backImage?: string | null; + imageFit?: 'cover' | 'contain'; + lanyardImage?: string | null; } export default function Lanyard({ position = [0, 0, 30], gravity = [0, -40, 0], fov = 20, - transparent = true + transparent = true, + frontImage = null, + backImage = null, + imageFit = 'cover', + lanyardImage = null }: LanyardProps) { const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768); @@ -54,7 +74,13 @@ export default function Lanyard({ > - + (null); const fixed = useRef(null); @@ -120,7 +158,58 @@ function Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }: BandProps) { }; const { nodes, materials } = useGLTF(cardGLB) as any; - const texture = useTexture(lanyard); + const texture = useTexture(lanyardImage || lanyard); + // useTexture must be called unconditionally; use a blank pixel when an image + // isn't supplied for a given face, then skip compositing it below. + const frontTex = useTexture(frontImage || BLANK_PIXEL); + const backTex = useTexture(backImage || BLANK_PIXEL); + + // Composite the front/back images into the card's texture atlas (front = left + // half, back = right half). Each image is drawn aspect-preserving (no stretch). + const cardMap = useMemo(() => { + const baseMap = materials.base.map as THREE.Texture; + if (!frontImage && !backImage) return baseMap; + + const baseImg = baseMap.image as any; + const W = baseImg.width; + const H = baseImg.height; + const canvas = document.createElement('canvas'); + canvas.width = W; + canvas.height = H; + const ctx = canvas.getContext('2d'); + if (!ctx) return baseMap; + // Keep the original baked atlas for the card edges and any untouched face. + ctx.drawImage(baseImg, 0, 0, W, H); + + const drawFitted = (img: any, rect: typeof FRONT_UV_RECT) => { + const rx = rect.x * W; + const ry = rect.y * H; + const rw = rect.w * W; + const rh = rect.h * H; + const pick = imageFit === 'contain' ? Math.min : Math.max; + const scale = pick(rw / img.width, rh / img.height); + const dw = img.width * scale; + const dh = img.height * scale; + const dx = rx + (rw - dw) / 2; + const dy = ry + (rh - dh) / 2; + ctx.save(); + ctx.beginPath(); + ctx.rect(rx, ry, rw, rh); + ctx.clip(); + ctx.drawImage(img, dx, dy, dw, dh); + ctx.restore(); + }; + + if (frontImage && frontTex.image) drawFitted(frontTex.image, FRONT_UV_RECT); + if (backImage && backTex.image) drawFitted(backTex.image, BACK_UV_RECT); + + const composite = new THREE.CanvasTexture(canvas); + composite.colorSpace = THREE.SRGBColorSpace; + composite.flipY = baseMap.flipY; + composite.anisotropy = 16; + composite.needsUpdate = true; + return composite; + }, [frontImage, backImage, imageFit, frontTex, backTex, materials.base.map]); const [curve] = useState( () => new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]) @@ -216,7 +305,7 @@ function Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }: BandProps) { > Date: Sun, 7 Jun 2026 19:00:14 +0800 Subject: [PATCH 04/12] feat(Lanyard): render card front/back from separate images (TS/Tailwind) Mirror the front/back compositing API (frontImage, backImage, imageFit) in the TypeScript + Tailwind variant. Default rendering is preserved when no custom image is provided. --- .../Components/Lanyard/Lanyard.tsx | 101 ++++++++++++++++-- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/src/ts-tailwind/Components/Lanyard/Lanyard.tsx b/src/ts-tailwind/Components/Lanyard/Lanyard.tsx index a0760da06..4905cd231 100644 --- a/src/ts-tailwind/Components/Lanyard/Lanyard.tsx +++ b/src/ts-tailwind/Components/Lanyard/Lanyard.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-unknown-property */ 'use client'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { Canvas, extend, useFrame } from '@react-three/fiber'; import { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei'; import { @@ -21,18 +21,38 @@ import lanyard from './lanyard.png'; extend({ MeshLineGeometry, MeshLineMaterial }); +// 1x1 transparent pixel — lets useTexture be called unconditionally when a +// front/back image isn't supplied. +const BLANK_PIXEL = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; + +// The card model's front face is UV-mapped to the LEFT half of the texture +// atlas and the back face to the RIGHT half (measured from card.glb). Each +// custom image is composited into its own half so the two faces render +// independently, aspect-preserving (no stretching). +const FRONT_UV_RECT = { x: 0, y: 0, w: 0.5, h: 0.755 }; +const BACK_UV_RECT = { x: 0.5, y: 0, w: 0.5, h: 0.757 }; + interface LanyardProps { position?: [number, number, number]; gravity?: [number, number, number]; fov?: number; transparent?: boolean; + frontImage?: string | null; + backImage?: string | null; + imageFit?: 'cover' | 'contain'; + lanyardImage?: string | null; } export default function Lanyard({ position = [0, 0, 30], gravity = [0, -40, 0], fov = 20, - transparent = true + transparent = true, + frontImage = null, + backImage = null, + imageFit = 'cover', + lanyardImage = null }: LanyardProps) { const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768); @@ -52,7 +72,13 @@ export default function Lanyard({ > - + (null); const fixed = useRef(null); @@ -118,7 +156,58 @@ function Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }: BandProps) { }; const { nodes, materials } = useGLTF(cardGLB) as any; - const texture = useTexture(lanyard); + const texture = useTexture(lanyardImage || lanyard); + // useTexture must be called unconditionally; use a blank pixel when an image + // isn't supplied for a given face, then skip compositing it below. + const frontTex = useTexture(frontImage || BLANK_PIXEL); + const backTex = useTexture(backImage || BLANK_PIXEL); + + // Composite the front/back images into the card's texture atlas (front = left + // half, back = right half). Each image is drawn aspect-preserving (no stretch). + const cardMap = useMemo(() => { + const baseMap = materials.base.map as THREE.Texture; + if (!frontImage && !backImage) return baseMap; + + const baseImg = baseMap.image as any; + const W = baseImg.width; + const H = baseImg.height; + const canvas = document.createElement('canvas'); + canvas.width = W; + canvas.height = H; + const ctx = canvas.getContext('2d'); + if (!ctx) return baseMap; + // Keep the original baked atlas for the card edges and any untouched face. + ctx.drawImage(baseImg, 0, 0, W, H); + + const drawFitted = (img: any, rect: typeof FRONT_UV_RECT) => { + const rx = rect.x * W; + const ry = rect.y * H; + const rw = rect.w * W; + const rh = rect.h * H; + const pick = imageFit === 'contain' ? Math.min : Math.max; + const scale = pick(rw / img.width, rh / img.height); + const dw = img.width * scale; + const dh = img.height * scale; + const dx = rx + (rw - dw) / 2; + const dy = ry + (rh - dh) / 2; + ctx.save(); + ctx.beginPath(); + ctx.rect(rx, ry, rw, rh); + ctx.clip(); + ctx.drawImage(img, dx, dy, dw, dh); + ctx.restore(); + }; + + if (frontImage && frontTex.image) drawFitted(frontTex.image, FRONT_UV_RECT); + if (backImage && backTex.image) drawFitted(backTex.image, BACK_UV_RECT); + + const composite = new THREE.CanvasTexture(canvas); + composite.colorSpace = THREE.SRGBColorSpace; + composite.flipY = baseMap.flipY; + composite.anisotropy = 16; + composite.needsUpdate = true; + return composite; + }, [frontImage, backImage, imageFit, frontTex, backTex, materials.base.map]); const [curve] = useState( () => new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]) @@ -214,7 +303,7 @@ function Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }: BandProps) { > Date: Sun, 7 Jun 2026 19:00:14 +0800 Subject: [PATCH 05/12] docs(Lanyard): document frontImage/backImage/imageFit in the demo prop table --- src/demo/Components/LanyardDemo.jsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/demo/Components/LanyardDemo.jsx b/src/demo/Components/LanyardDemo.jsx index 98c45fab2..66ab3402b 100644 --- a/src/demo/Components/LanyardDemo.jsx +++ b/src/demo/Components/LanyardDemo.jsx @@ -53,6 +53,30 @@ const LanyardDemo = () => { type: 'boolean', default: 'true', description: 'Enables a transparent background for the canvas.' + }, + { + name: 'frontImage', + type: 'string', + default: 'null', + description: "Custom image URL for the card's front face. Falls back to the model's built-in texture when not set." + }, + { + name: 'backImage', + type: 'string', + default: 'null', + description: "Custom image URL for the card's back face, rendered independently from the front." + }, + { + name: 'imageFit', + type: '"cover" | "contain"', + default: '"cover"', + description: "How a custom front/back image fits its face. Both preserve aspect ratio; 'cover' fills and crops, 'contain' letterboxes." + }, + { + name: 'lanyardImage', + type: 'string', + default: 'null', + description: "Custom image URL for the lanyard band's repeating texture. Falls back to the default band texture when not set." } ], [] From a49c9f9d7e9d31cc2db668348bd5729d3516d18e Mon Sep 17 00:00:00 2001 From: EnderRomantice Date: Sun, 7 Jun 2026 19:00:14 +0800 Subject: [PATCH 06/12] docs(Lanyard): update usage snippet for front/back card images --- src/constants/code/Components/lanyardCode.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/constants/code/Components/lanyardCode.js b/src/constants/code/Components/lanyardCode.js index fc3a54cf9..24c88761e 100644 --- a/src/constants/code/Components/lanyardCode.js +++ b/src/constants/code/Components/lanyardCode.js @@ -10,6 +10,17 @@ export const lanyard = { +// Pass custom images for the card's front/back faces and/or the lanyard band. +// frontImage and backImage render independently; imageFit keeps aspect ratio. + + /* IMPORTANT INFO BELOW 1. You MUST have the card.glb and lanyard.png files in your project and import them @@ -17,6 +28,7 @@ export const lanyard = { 2. You can edit your card.glb file in this online .glb editor and change the texture: - https://modelviewer.dev/editor/ +- alternatively, pass the "frontImage" / "backImage" props to swap the card's faces at runtime 4. The png file is the texture for the lanyard's band and can be edited in any image editor From d56cf1538e31f6ce2174e82bf3351b0f198bb1f8 Mon Sep 17 00:00:00 2001 From: EnderRomantice Date: Sun, 7 Jun 2026 20:20:36 +0800 Subject: [PATCH 07/12] feat(Lanyard): add lanyardWidth prop to size the band (JS/CSS) Expose the band's meshline lineWidth as a `lanyardWidth` prop (default 1). Widening the band gives a custom band image more room and reduces texture stretching. Default value preserves the current appearance. --- src/content/Components/Lanyard/Lanyard.jsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/content/Components/Lanyard/Lanyard.jsx b/src/content/Components/Lanyard/Lanyard.jsx index ae3c4dfa8..86dae4652 100644 --- a/src/content/Components/Lanyard/Lanyard.jsx +++ b/src/content/Components/Lanyard/Lanyard.jsx @@ -35,7 +35,8 @@ export default function Lanyard({ frontImage = null, backImage = null, imageFit = 'cover', - lanyardImage = null + lanyardImage = null, + lanyardWidth = 1 }) { const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768); @@ -61,6 +62,7 @@ export default function Lanyard({ backImage={backImage} imageFit={imageFit} lanyardImage={lanyardImage} + lanyardWidth={lanyardWidth} /> @@ -104,7 +106,8 @@ function Band({ frontImage = null, backImage = null, imageFit = 'cover', - lanyardImage = null + lanyardImage = null, + lanyardWidth = 1 }) { const band = useRef(), fixed = useRef(), @@ -273,7 +276,7 @@ function Band({ useMap map={texture} repeat={[-4, 1]} - lineWidth={1} + lineWidth={lanyardWidth} /> From de14991b808b01dd049d450832a5c0fbaa448c71 Mon Sep 17 00:00:00 2001 From: EnderRomantice Date: Sun, 7 Jun 2026 20:20:36 +0800 Subject: [PATCH 08/12] feat(Lanyard): add lanyardWidth prop to size the band (JS/Tailwind) Mirror the lanyardWidth prop (maps to meshline lineWidth, default 1) in the JavaScript + Tailwind variant. --- src/tailwind/Components/Lanyard/Lanyard.jsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/tailwind/Components/Lanyard/Lanyard.jsx b/src/tailwind/Components/Lanyard/Lanyard.jsx index 09995a315..76e23af8a 100644 --- a/src/tailwind/Components/Lanyard/Lanyard.jsx +++ b/src/tailwind/Components/Lanyard/Lanyard.jsx @@ -34,7 +34,8 @@ export default function Lanyard({ frontImage = null, backImage = null, imageFit = 'cover', - lanyardImage = null + lanyardImage = null, + lanyardWidth = 1 }) { const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768); @@ -60,6 +61,7 @@ export default function Lanyard({ backImage={backImage} imageFit={imageFit} lanyardImage={lanyardImage} + lanyardWidth={lanyardWidth} /> @@ -103,7 +105,8 @@ function Band({ frontImage = null, backImage = null, imageFit = 'cover', - lanyardImage = null + lanyardImage = null, + lanyardWidth = 1 }) { const band = useRef(), fixed = useRef(), @@ -272,7 +275,7 @@ function Band({ useMap map={texture} repeat={[-4, 1]} - lineWidth={1} + lineWidth={lanyardWidth} /> From c57cc945bfb25fca3a31c3c9b7d5bb946cb34a31 Mon Sep 17 00:00:00 2001 From: EnderRomantice Date: Sun, 7 Jun 2026 20:20:36 +0800 Subject: [PATCH 09/12] feat(Lanyard): add lanyardWidth prop to size the band (TS/CSS) Mirror the lanyardWidth prop (maps to meshline lineWidth, default 1) in the TypeScript + CSS variant, typed on LanyardProps and BandProps. --- src/ts-default/Components/Lanyard/Lanyard.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ts-default/Components/Lanyard/Lanyard.tsx b/src/ts-default/Components/Lanyard/Lanyard.tsx index e4f239ab7..41235a7e2 100644 --- a/src/ts-default/Components/Lanyard/Lanyard.tsx +++ b/src/ts-default/Components/Lanyard/Lanyard.tsx @@ -44,6 +44,7 @@ interface LanyardProps { backImage?: string | null; imageFit?: 'cover' | 'contain'; lanyardImage?: string | null; + lanyardWidth?: number; } export default function Lanyard({ @@ -54,7 +55,8 @@ export default function Lanyard({ frontImage = null, backImage = null, imageFit = 'cover', - lanyardImage = null + lanyardImage = null, + lanyardWidth = 1 }: LanyardProps) { const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768); @@ -80,6 +82,7 @@ export default function Lanyard({ backImage={backImage} imageFit={imageFit} lanyardImage={lanyardImage} + lanyardWidth={lanyardWidth} /> @@ -125,6 +128,7 @@ interface BandProps { backImage?: string | null; imageFit?: 'cover' | 'contain'; lanyardImage?: string | null; + lanyardWidth?: number; } function Band({ @@ -134,7 +138,8 @@ function Band({ frontImage = null, backImage = null, imageFit = 'cover', - lanyardImage = null + lanyardImage = null, + lanyardWidth = 1 }: BandProps) { // Using "any" for refs since the exact types depend on Rapier's internals const band = useRef(null); @@ -327,7 +332,7 @@ function Band({ useMap map={texture} repeat={[-4, 1]} - lineWidth={1} + lineWidth={lanyardWidth} /> From ff7801052057c336b6a7ceed2c7d37d76248a53f Mon Sep 17 00:00:00 2001 From: EnderRomantice Date: Sun, 7 Jun 2026 20:20:36 +0800 Subject: [PATCH 10/12] feat(Lanyard): add lanyardWidth prop to size the band (TS/Tailwind) Mirror the lanyardWidth prop (maps to meshline lineWidth, default 1) in the TypeScript + Tailwind variant. --- src/ts-tailwind/Components/Lanyard/Lanyard.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ts-tailwind/Components/Lanyard/Lanyard.tsx b/src/ts-tailwind/Components/Lanyard/Lanyard.tsx index 4905cd231..bf2a8fdc6 100644 --- a/src/ts-tailwind/Components/Lanyard/Lanyard.tsx +++ b/src/ts-tailwind/Components/Lanyard/Lanyard.tsx @@ -42,6 +42,7 @@ interface LanyardProps { backImage?: string | null; imageFit?: 'cover' | 'contain'; lanyardImage?: string | null; + lanyardWidth?: number; } export default function Lanyard({ @@ -52,7 +53,8 @@ export default function Lanyard({ frontImage = null, backImage = null, imageFit = 'cover', - lanyardImage = null + lanyardImage = null, + lanyardWidth = 1 }: LanyardProps) { const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768); @@ -78,6 +80,7 @@ export default function Lanyard({ backImage={backImage} imageFit={imageFit} lanyardImage={lanyardImage} + lanyardWidth={lanyardWidth} /> @@ -123,6 +126,7 @@ interface BandProps { backImage?: string | null; imageFit?: 'cover' | 'contain'; lanyardImage?: string | null; + lanyardWidth?: number; } function Band({ @@ -132,7 +136,8 @@ function Band({ frontImage = null, backImage = null, imageFit = 'cover', - lanyardImage = null + lanyardImage = null, + lanyardWidth = 1 }: BandProps) { // Using "any" for refs since the exact types depend on Rapier's internals const band = useRef(null); @@ -325,7 +330,7 @@ function Band({ useMap map={texture} repeat={[-4, 1]} - lineWidth={1} + lineWidth={lanyardWidth} /> From 4a4ef0ba4e3a5383142017e039cf27cd172dfa7d Mon Sep 17 00:00:00 2001 From: EnderRomantice Date: Sun, 7 Jun 2026 20:20:36 +0800 Subject: [PATCH 11/12] docs(Lanyard): document lanyardWidth in the demo prop table --- src/demo/Components/LanyardDemo.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/demo/Components/LanyardDemo.jsx b/src/demo/Components/LanyardDemo.jsx index 66ab3402b..41d5e7590 100644 --- a/src/demo/Components/LanyardDemo.jsx +++ b/src/demo/Components/LanyardDemo.jsx @@ -77,6 +77,12 @@ const LanyardDemo = () => { type: 'string', default: 'null', description: "Custom image URL for the lanyard band's repeating texture. Falls back to the default band texture when not set." + }, + { + name: 'lanyardWidth', + type: 'number', + default: '1', + description: 'Width of the lanyard band (meshline lineWidth). Increase it to give a custom band image more room and reduce stretching.' } ], [] From 91aab0151b6e4db06de119bd75857c2f381b1edd Mon Sep 17 00:00:00 2001 From: EnderRomantice Date: Sun, 7 Jun 2026 20:20:36 +0800 Subject: [PATCH 12/12] docs(Lanyard): mention lanyardWidth in the usage snippet --- src/constants/code/Components/lanyardCode.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/constants/code/Components/lanyardCode.js b/src/constants/code/Components/lanyardCode.js index 24c88761e..f5208f940 100644 --- a/src/constants/code/Components/lanyardCode.js +++ b/src/constants/code/Components/lanyardCode.js @@ -12,6 +12,7 @@ export const lanyard = { // Pass custom images for the card's front/back faces and/or the lanyard band. // frontImage and backImage render independently; imageFit keeps aspect ratio. +// lanyardWidth widens the band so a custom band image has more room. /* IMPORTANT INFO BELOW