diff --git a/src/constants/code/Components/lanyardCode.js b/src/constants/code/Components/lanyardCode.js
index fc3a54cf9..f5208f940 100644
--- a/src/constants/code/Components/lanyardCode.js
+++ b/src/constants/code/Components/lanyardCode.js
@@ -10,6 +10,19 @@ 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
1. You MUST have the card.glb and lanyard.png files in your project and import them
@@ -17,6 +30,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
diff --git a/src/content/Components/Lanyard/Lanyard.jsx b/src/content/Components/Lanyard/Lanyard.jsx
index 4c010ede4..86dae4652 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,29 @@ 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,
+ lanyardWidth = 1
+}) {
const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);
useEffect(() => {
@@ -34,7 +56,14 @@ 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,
+ lanyardWidth = 1
+}) {
const band = useRef(),
fixed = useRef(),
j1 = useRef(),
@@ -83,7 +121,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 +254,7 @@ function Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }) {
>
>
diff --git a/src/demo/Components/LanyardDemo.jsx b/src/demo/Components/LanyardDemo.jsx
index 98c45fab2..41d5e7590 100644
--- a/src/demo/Components/LanyardDemo.jsx
+++ b/src/demo/Components/LanyardDemo.jsx
@@ -53,6 +53,36 @@ 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."
+ },
+ {
+ 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.'
}
],
[]
diff --git a/src/tailwind/Components/Lanyard/Lanyard.jsx b/src/tailwind/Components/Lanyard/Lanyard.jsx
index fdb62506b..76e23af8a 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,29 @@ 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,
+ lanyardWidth = 1
+}) {
const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);
useEffect(() => {
@@ -33,7 +55,14 @@ 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,
+ lanyardWidth = 1
+}) {
const band = useRef(),
fixed = useRef(),
j1 = useRef(),
@@ -82,7 +120,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 +253,7 @@ function Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }) {
>
>
diff --git a/src/ts-default/Components/Lanyard/Lanyard.tsx b/src/ts-default/Components/Lanyard/Lanyard.tsx
index 661c015b8..41235a7e2 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,40 @@ 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;
+ lanyardWidth?: number;
}
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,
+ lanyardWidth = 1
}: LanyardProps) {
const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);
@@ -54,7 +76,14 @@ export default function Lanyard({
>
-
+
(null);
const fixed = useRef(null);
@@ -120,7 +163,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 +310,7 @@ function Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }: BandProps) {
>
>
diff --git a/src/ts-tailwind/Components/Lanyard/Lanyard.tsx b/src/ts-tailwind/Components/Lanyard/Lanyard.tsx
index a0760da06..bf2a8fdc6 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,40 @@ 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;
+ lanyardWidth?: number;
}
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,
+ lanyardWidth = 1
}: LanyardProps) {
const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);
@@ -52,7 +74,14 @@ export default function Lanyard({
>
-
+
(null);
const fixed = useRef(null);
@@ -118,7 +161,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 +308,7 @@ function Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }: BandProps) {
>
>