{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "infinite-menu",
  "type": "registry:block",
  "title": "Infinite menu",
  "description": "Infinite menu",
  "files": [
    {
      "path": "components/usages/infinitemenuusage.tsx",
      "content": "\"use client\";\r\n\r\nimport React from \"react\";\r\n\r\nimport InfiniteMenu from \"@/registry/open-source/infinite-menu\";\r\n\r\nexport default function Usage() {\r\n\treturn (\r\n\t\t<div className=\"h-screen w-full flex items-center justify-center relative overflow-hidden bg-background\">\r\n\t\t\t<div style={{ height: \"600px\", position: \"relative\" }}>\r\n\t\t\t\t<InfiniteMenu\r\n\t\t\t\t\titems={[\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\timage: \"/itjustworks.jpg\",\r\n\t\t\t\t\t\t\tlink: \"https://google.com/\",\r\n\t\t\t\t\t\t\ttitle: \"Item 1\",\r\n\t\t\t\t\t\t\tdescription: \"This is pretty cool, right?\",\r\n\t\t\t\t\t\t},\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\timage: \"/itjustworks.jpg\",\r\n\t\t\t\t\t\t\tlink: \"https://google.com/\",\r\n\t\t\t\t\t\t\ttitle: \"Item 2\",\r\n\t\t\t\t\t\t\tdescription: \"This is pretty cool, right?\",\r\n\t\t\t\t\t\t},\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\timage: \"/itjustworks.jpg\",\r\n\t\t\t\t\t\t\tlink: \"https://google.com/\",\r\n\t\t\t\t\t\t\ttitle: \"Item 3\",\r\n\t\t\t\t\t\t\tdescription: \"This is pretty cool, right?\",\r\n\t\t\t\t\t\t},\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\timage: \"/itjustworks.jpg\",\r\n\t\t\t\t\t\t\tlink: \"https://google.com/\",\r\n\t\t\t\t\t\t\ttitle: \"Item 4\",\r\n\t\t\t\t\t\t\tdescription: \"This is pretty cool, right?\",\r\n\t\t\t\t\t\t},\r\n\t\t\t\t\t]}\r\n\t\t\t\t/>\r\n\t\t\t</div>\r\n\t\t</div>\r\n\t);\r\n}\r\n",
      "type": "registry:block",
      "target": "~/example.tsx"
    },
    {
      "path": "components/usages/infinitemenuusage.tsx",
      "content": "\"use client\";\r\n\r\nimport React from \"react\";\r\n\r\nimport InfiniteMenu from \"@/registry/open-source/infinite-menu\";\r\n\r\nexport default function Usage() {\r\n\treturn (\r\n\t\t<div className=\"h-screen w-full flex items-center justify-center relative overflow-hidden bg-background\">\r\n\t\t\t<div style={{ height: \"600px\", position: \"relative\" }}>\r\n\t\t\t\t<InfiniteMenu\r\n\t\t\t\t\titems={[\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\timage: \"/itjustworks.jpg\",\r\n\t\t\t\t\t\t\tlink: \"https://google.com/\",\r\n\t\t\t\t\t\t\ttitle: \"Item 1\",\r\n\t\t\t\t\t\t\tdescription: \"This is pretty cool, right?\",\r\n\t\t\t\t\t\t},\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\timage: \"/itjustworks.jpg\",\r\n\t\t\t\t\t\t\tlink: \"https://google.com/\",\r\n\t\t\t\t\t\t\ttitle: \"Item 2\",\r\n\t\t\t\t\t\t\tdescription: \"This is pretty cool, right?\",\r\n\t\t\t\t\t\t},\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\timage: \"/itjustworks.jpg\",\r\n\t\t\t\t\t\t\tlink: \"https://google.com/\",\r\n\t\t\t\t\t\t\ttitle: \"Item 3\",\r\n\t\t\t\t\t\t\tdescription: \"This is pretty cool, right?\",\r\n\t\t\t\t\t\t},\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\timage: \"/itjustworks.jpg\",\r\n\t\t\t\t\t\t\tlink: \"https://google.com/\",\r\n\t\t\t\t\t\t\ttitle: \"Item 4\",\r\n\t\t\t\t\t\t\tdescription: \"This is pretty cool, right?\",\r\n\t\t\t\t\t\t},\r\n\t\t\t\t\t]}\r\n\t\t\t\t/>\r\n\t\t\t</div>\r\n\t\t</div>\r\n\t);\r\n}\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/open-source/infinite-menu.tsx",
      "content": "import { FC, MutableRefObject, useEffect, useRef, useState } from \"react\";\r\n\r\nimport { mat4, quat, vec2, vec3 } from \"gl-matrix\";\r\n\r\n// Credit:\r\n// https://www.reactbits.dev/components/infinite-menu\r\n\r\n// example usage\r\n// const items = [\r\n//   {\r\n//     image: 'https://picsum.photos/300/300?grayscale',\r\n//     link: 'https://google.com/',\r\n//     title: 'Item 1',\r\n//     description: 'This is pretty cool, right?'\r\n//   },\r\n//   {\r\n//     image: 'https://picsum.photos/400/400?grayscale',\r\n//     link: 'https://google.com/',\r\n//     title: 'Item 2',\r\n//     description: 'This is pretty cool, right?'\r\n//   },\r\n//   {\r\n//     image: 'https://picsum.photos/500/500?grayscale',\r\n//     link: 'https://google.com/',\r\n//     title: 'Item 3',\r\n//     description: 'This is pretty cool, right?'\r\n//   },\r\n//   {\r\n//     image: 'https://picsum.photos/600/600?grayscale',\r\n//     link: 'https://google.com/',\r\n//     title: 'Item 4',\r\n//     description: 'This is pretty cool, right?'\r\n//   }\r\n// ];\r\n\r\n// <div style={{ height: '600px', position: 'relative' }}>\r\n//   <InfiniteMenu items={items}/>\r\n// </div>\r\n\r\n// -------- Shader Sources --------\r\n\r\nconst discVertShaderSource = `#version 300 es\r\n\r\nuniform mat4 uWorldMatrix;\r\nuniform mat4 uViewMatrix;\r\nuniform mat4 uProjectionMatrix;\r\nuniform vec3 uCameraPosition;\r\nuniform vec4 uRotationAxisVelocity;\r\n\r\nin vec3 aModelPosition;\r\nin vec3 aModelNormal; // not currently used, but reserved\r\nin vec2 aModelUvs;\r\nin mat4 aInstanceMatrix;\r\n\r\nout vec2 vUvs;\r\nout float vAlpha;\r\nflat out int vInstanceId;\r\n\r\n#define PI 3.141593\r\n\r\nvoid main() {\r\n    vec4 worldPosition = uWorldMatrix * aInstanceMatrix * vec4(aModelPosition, 1.);\r\n\r\n    // center of the disc in world space\r\n    vec3 centerPos = (uWorldMatrix * aInstanceMatrix * vec4(0., 0., 0., 1.)).xyz;\r\n    float radius = length(centerPos.xyz);\r\n\r\n    // skip the center vertex of the disc geometry\r\n    if (gl_VertexID > 0) {\r\n        // stretch the disc according to the axis and velocity of the rotation\r\n        vec3 rotationAxis = uRotationAxisVelocity.xyz;\r\n        float rotationVelocity = min(.15, uRotationAxisVelocity.w * 15.);\r\n        // the stretch direction is orthogonal to the rotation axis and the position\r\n        vec3 stretchDir = normalize(cross(centerPos, rotationAxis));\r\n        // the position of this vertex relative to the center position\r\n        vec3 relativeVertexPos = normalize(worldPosition.xyz - centerPos);\r\n        // vertices more in line with the stretch direction get a larger offset\r\n        float strength = dot(stretchDir, relativeVertexPos);\r\n        float invAbsStrength = min(0., abs(strength) - 1.);\r\n        strength = rotationVelocity * sign(strength) * abs(invAbsStrength * invAbsStrength * invAbsStrength + 1.);\r\n        // apply the stretch distortion\r\n        worldPosition.xyz += stretchDir * strength;\r\n    }\r\n\r\n    // move the vertex back to the overall sphere\r\n    worldPosition.xyz = radius * normalize(worldPosition.xyz);\r\n\r\n    gl_Position = uProjectionMatrix * uViewMatrix * worldPosition;\r\n\r\n    vAlpha = smoothstep(0.5, 1., normalize(worldPosition.xyz).z) * .9 + .1;\r\n    vUvs = aModelUvs;\r\n    vInstanceId = gl_InstanceID;\r\n}\r\n`;\r\n\r\nconst discFragShaderSource = `#version 300 es\r\nprecision highp float;\r\n\r\nuniform sampler2D uTex;\r\nuniform int uItemCount;\r\nuniform int uAtlasSize;\r\n\r\nout vec4 outColor;\r\n\r\nin vec2 vUvs;\r\nin float vAlpha;\r\nflat in int vInstanceId;\r\n\r\nvoid main() {\r\n    // Calculate which item to display based on instance ID\r\n    int itemIndex = vInstanceId % uItemCount;\r\n    int cellsPerRow = uAtlasSize;\r\n    int cellX = itemIndex % cellsPerRow;\r\n    int cellY = itemIndex / cellsPerRow;\r\n    vec2 cellSize = vec2(1.0) / vec2(float(cellsPerRow));\r\n    vec2 cellOffset = vec2(float(cellX), float(cellY)) * cellSize;\r\n\r\n    // Get texture dimensions and calculate aspect ratio\r\n    ivec2 texSize = textureSize(uTex, 0);\r\n    float imageAspect = float(texSize.x) / float(texSize.y);\r\n    float containerAspect = 1.0; // Assuming square container\r\n    \r\n    // Calculate cover scale factor\r\n    float scale = max(imageAspect / containerAspect, \r\n                     containerAspect / imageAspect);\r\n    \r\n    // Rotate 180 degrees and adjust UVs for cover\r\n    vec2 st = 1.0 - vUvs; // 180 degree rotation\r\n    st = (st - 0.5) * scale + 0.5;\r\n    \r\n    // Clamp coordinates to prevent repeating\r\n    st = clamp(st, 0.0, 1.0);\r\n    \r\n    // Map to the correct cell in the atlas\r\n    st = st * cellSize + cellOffset;\r\n    \r\n    outColor = texture(uTex, st);\r\n    outColor.a *= vAlpha;\r\n}\r\n`;\r\n\r\n// -------- Geometry Classes --------\r\n\r\nclass Face {\r\n\tpublic a: number;\r\n\tpublic b: number;\r\n\tpublic c: number;\r\n\r\n\tconstructor(a: number, b: number, c: number) {\r\n\t\tthis.a = a;\r\n\t\tthis.b = b;\r\n\t\tthis.c = c;\r\n\t}\r\n}\r\n\r\nclass Vertex {\r\n\tpublic position: vec3;\r\n\tpublic normal: vec3;\r\n\tpublic uv: vec2;\r\n\r\n\tconstructor(x: number, y: number, z: number) {\r\n\t\tthis.position = vec3.fromValues(x, y, z);\r\n\t\tthis.normal = vec3.create();\r\n\t\tthis.uv = vec2.create();\r\n\t}\r\n}\r\n\r\nclass Geometry {\r\n\tpublic vertices: Vertex[];\r\n\tpublic faces: Face[];\r\n\r\n\tconstructor() {\r\n\t\tthis.vertices = [];\r\n\t\tthis.faces = [];\r\n\t}\r\n\r\n\tpublic addVertex(...args: number[]): this {\r\n\t\tfor (let i = 0; i < args.length; i += 3) {\r\n\t\t\tthis.vertices.push(new Vertex(args[i], args[i + 1], args[i + 2]));\r\n\t\t}\r\n\t\treturn this;\r\n\t}\r\n\r\n\tpublic addFace(...args: number[]): this {\r\n\t\tfor (let i = 0; i < args.length; i += 3) {\r\n\t\t\tthis.faces.push(new Face(args[i], args[i + 1], args[i + 2]));\r\n\t\t}\r\n\t\treturn this;\r\n\t}\r\n\r\n\tpublic get lastVertex(): Vertex {\r\n\t\treturn this.vertices[this.vertices.length - 1];\r\n\t}\r\n\r\n\tpublic subdivide(divisions = 1): this {\r\n\t\tconst midPointCache: Record<string, number> = {};\r\n\t\tlet f = this.faces;\r\n\r\n\t\tfor (let div = 0; div < divisions; ++div) {\r\n\t\t\tconst newFaces = new Array<Face>(f.length * 4);\r\n\r\n\t\t\tf.forEach((face, ndx) => {\r\n\t\t\t\tconst mAB = this.getMidPoint(face.a, face.b, midPointCache);\r\n\t\t\t\tconst mBC = this.getMidPoint(face.b, face.c, midPointCache);\r\n\t\t\t\tconst mCA = this.getMidPoint(face.c, face.a, midPointCache);\r\n\r\n\t\t\t\tconst i = ndx * 4;\r\n\t\t\t\tnewFaces[i + 0] = new Face(face.a, mAB, mCA);\r\n\t\t\t\tnewFaces[i + 1] = new Face(face.b, mBC, mAB);\r\n\t\t\t\tnewFaces[i + 2] = new Face(face.c, mCA, mBC);\r\n\t\t\t\tnewFaces[i + 3] = new Face(mAB, mBC, mCA);\r\n\t\t\t});\r\n\r\n\t\t\tf = newFaces;\r\n\t\t}\r\n\r\n\t\tthis.faces = f;\r\n\t\treturn this;\r\n\t}\r\n\r\n\tpublic spherize(radius = 1): this {\r\n\t\tthis.vertices.forEach((vertex) => {\r\n\t\t\tvec3.normalize(vertex.normal, vertex.position);\r\n\t\t\tvec3.scale(vertex.position, vertex.normal, radius);\r\n\t\t});\r\n\t\treturn this;\r\n\t}\r\n\r\n\tpublic get data(): {\r\n\t\tvertices: Float32Array;\r\n\t\tindices: Uint16Array;\r\n\t\tnormals: Float32Array;\r\n\t\tuvs: Float32Array;\r\n\t} {\r\n\t\treturn {\r\n\t\t\tvertices: this.vertexData,\r\n\t\t\tindices: this.indexData,\r\n\t\t\tnormals: this.normalData,\r\n\t\t\tuvs: this.uvData,\r\n\t\t};\r\n\t}\r\n\r\n\tpublic get vertexData(): Float32Array {\r\n\t\treturn new Float32Array(\r\n\t\t\tthis.vertices.flatMap((v) => Array.from(v.position))\r\n\t\t);\r\n\t}\r\n\r\n\tpublic get normalData(): Float32Array {\r\n\t\treturn new Float32Array(\r\n\t\t\tthis.vertices.flatMap((v) => Array.from(v.normal))\r\n\t\t);\r\n\t}\r\n\r\n\tpublic get uvData(): Float32Array {\r\n\t\treturn new Float32Array(this.vertices.flatMap((v) => Array.from(v.uv)));\r\n\t}\r\n\r\n\tpublic get indexData(): Uint16Array {\r\n\t\treturn new Uint16Array(this.faces.flatMap((f) => [f.a, f.b, f.c]));\r\n\t}\r\n\r\n\tpublic getMidPoint(\r\n\t\tndxA: number,\r\n\t\tndxB: number,\r\n\t\tcache: Record<string, number>\r\n\t): number {\r\n\t\tconst cacheKey = ndxA < ndxB ? `k_${ndxB}_${ndxA}` : `k_${ndxA}_${ndxB}`;\r\n\t\tif (Object.prototype.hasOwnProperty.call(cache, cacheKey)) {\r\n\t\t\treturn cache[cacheKey];\r\n\t\t}\r\n\t\tconst a = this.vertices[ndxA].position;\r\n\t\tconst b = this.vertices[ndxB].position;\r\n\t\tconst ndx = this.vertices.length;\r\n\t\tcache[cacheKey] = ndx;\r\n\t\tthis.addVertex(\r\n\t\t\t(a[0] + b[0]) * 0.5,\r\n\t\t\t(a[1] + b[1]) * 0.5,\r\n\t\t\t(a[2] + b[2]) * 0.5\r\n\t\t);\r\n\t\treturn ndx;\r\n\t}\r\n}\r\n\r\nclass IcosahedronGeometry extends Geometry {\r\n\tconstructor() {\r\n\t\tsuper();\r\n\t\tconst t = Math.sqrt(5) * 0.5 + 0.5;\r\n\t\tthis.addVertex(\r\n\t\t\t-1,\r\n\t\t\tt,\r\n\t\t\t0,\r\n\t\t\t1,\r\n\t\t\tt,\r\n\t\t\t0,\r\n\t\t\t-1,\r\n\t\t\t-t,\r\n\t\t\t0,\r\n\t\t\t1,\r\n\t\t\t-t,\r\n\t\t\t0,\r\n\t\t\t0,\r\n\t\t\t-1,\r\n\t\t\tt,\r\n\t\t\t0,\r\n\t\t\t1,\r\n\t\t\tt,\r\n\t\t\t0,\r\n\t\t\t-1,\r\n\t\t\t-t,\r\n\t\t\t0,\r\n\t\t\t1,\r\n\t\t\t-t,\r\n\t\t\tt,\r\n\t\t\t0,\r\n\t\t\t-1,\r\n\t\t\tt,\r\n\t\t\t0,\r\n\t\t\t1,\r\n\t\t\t-t,\r\n\t\t\t0,\r\n\t\t\t-1,\r\n\t\t\t-t,\r\n\t\t\t0,\r\n\t\t\t1\r\n\t\t).addFace(\r\n\t\t\t0,\r\n\t\t\t11,\r\n\t\t\t5,\r\n\t\t\t0,\r\n\t\t\t5,\r\n\t\t\t1,\r\n\t\t\t0,\r\n\t\t\t1,\r\n\t\t\t7,\r\n\t\t\t0,\r\n\t\t\t7,\r\n\t\t\t10,\r\n\t\t\t0,\r\n\t\t\t10,\r\n\t\t\t11,\r\n\t\t\t1,\r\n\t\t\t5,\r\n\t\t\t9,\r\n\t\t\t5,\r\n\t\t\t11,\r\n\t\t\t4,\r\n\t\t\t11,\r\n\t\t\t10,\r\n\t\t\t2,\r\n\t\t\t10,\r\n\t\t\t7,\r\n\t\t\t6,\r\n\t\t\t7,\r\n\t\t\t1,\r\n\t\t\t8,\r\n\t\t\t3,\r\n\t\t\t9,\r\n\t\t\t4,\r\n\t\t\t3,\r\n\t\t\t4,\r\n\t\t\t2,\r\n\t\t\t3,\r\n\t\t\t2,\r\n\t\t\t6,\r\n\t\t\t3,\r\n\t\t\t6,\r\n\t\t\t8,\r\n\t\t\t3,\r\n\t\t\t8,\r\n\t\t\t9,\r\n\t\t\t4,\r\n\t\t\t9,\r\n\t\t\t5,\r\n\t\t\t2,\r\n\t\t\t4,\r\n\t\t\t11,\r\n\t\t\t6,\r\n\t\t\t2,\r\n\t\t\t10,\r\n\t\t\t8,\r\n\t\t\t6,\r\n\t\t\t7,\r\n\t\t\t9,\r\n\t\t\t8,\r\n\t\t\t1\r\n\t\t);\r\n\t}\r\n}\r\n\r\nclass DiscGeometry extends Geometry {\r\n\tconstructor(steps = 4, radius = 1) {\r\n\t\tsuper();\r\n\t\tconst safeSteps = Math.max(4, steps);\r\n\t\tconst alpha = (2 * Math.PI) / safeSteps;\r\n\r\n\t\t// center vertex\r\n\t\tthis.addVertex(0, 0, 0);\r\n\t\tthis.lastVertex.uv[0] = 0.5;\r\n\t\tthis.lastVertex.uv[1] = 0.5;\r\n\r\n\t\tfor (let i = 0; i < safeSteps; ++i) {\r\n\t\t\tconst x = Math.cos(alpha * i);\r\n\t\t\tconst y = Math.sin(alpha * i);\r\n\t\t\tthis.addVertex(radius * x, radius * y, 0);\r\n\t\t\tthis.lastVertex.uv[0] = x * 0.5 + 0.5;\r\n\t\t\tthis.lastVertex.uv[1] = y * 0.5 + 0.5;\r\n\r\n\t\t\tif (i > 0) {\r\n\t\t\t\tthis.addFace(0, i, i + 1);\r\n\t\t\t}\r\n\t\t}\r\n\t\tthis.addFace(0, safeSteps, 1);\r\n\t}\r\n}\r\n\r\n// -------- WebGL Helpers --------\r\n\r\nfunction createShader(\r\n\tgl: WebGL2RenderingContext,\r\n\ttype: number,\r\n\tsource: string\r\n): WebGLShader | null {\r\n\tconst shader = gl.createShader(type);\r\n\tif (!shader) return null;\r\n\tgl.shaderSource(shader, source);\r\n\tgl.compileShader(shader);\r\n\tconst success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);\r\n\r\n\tif (success) {\r\n\t\treturn shader;\r\n\t}\r\n\r\n\tconsole.error(gl.getShaderInfoLog(shader));\r\n\tgl.deleteShader(shader);\r\n\treturn null;\r\n}\r\n\r\nfunction createProgram(\r\n\tgl: WebGL2RenderingContext,\r\n\tshaderSources: [string, string],\r\n\ttransformFeedbackVaryings?: string[] | null,\r\n\tattribLocations?: Record<string, number>\r\n): WebGLProgram | null {\r\n\tconst program = gl.createProgram();\r\n\tif (!program) return null;\r\n\r\n\t[gl.VERTEX_SHADER, gl.FRAGMENT_SHADER].forEach((type, ndx) => {\r\n\t\tconst shader = createShader(gl, type, shaderSources[ndx]);\r\n\t\tif (shader) {\r\n\t\t\tgl.attachShader(program, shader);\r\n\t\t}\r\n\t});\r\n\r\n\tif (transformFeedbackVaryings) {\r\n\t\tgl.transformFeedbackVaryings(\r\n\t\t\tprogram,\r\n\t\t\ttransformFeedbackVaryings,\r\n\t\t\tgl.SEPARATE_ATTRIBS\r\n\t\t);\r\n\t}\r\n\r\n\tif (attribLocations) {\r\n\t\tfor (const attrib in attribLocations) {\r\n\t\t\tif (Object.prototype.hasOwnProperty.call(attribLocations, attrib)) {\r\n\t\t\t\tgl.bindAttribLocation(program, attribLocations[attrib], attrib);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tgl.linkProgram(program);\r\n\tconst success = gl.getProgramParameter(program, gl.LINK_STATUS);\r\n\r\n\tif (success) {\r\n\t\treturn program;\r\n\t}\r\n\r\n\tconsole.error(gl.getProgramInfoLog(program));\r\n\tgl.deleteProgram(program);\r\n\treturn null;\r\n}\r\n\r\nfunction makeVertexArray(\r\n\tgl: WebGL2RenderingContext,\r\n\tbufLocNumElmPairs: Array<[WebGLBuffer, number, number]>,\r\n\tindices?: Uint16Array\r\n): WebGLVertexArrayObject | null {\r\n\tconst va = gl.createVertexArray();\r\n\tif (!va) return null;\r\n\r\n\tgl.bindVertexArray(va);\r\n\r\n\tfor (const [buffer, loc, numElem] of bufLocNumElmPairs) {\r\n\t\tif (loc === -1) continue;\r\n\t\tgl.bindBuffer(gl.ARRAY_BUFFER, buffer);\r\n\t\tgl.enableVertexAttribArray(loc);\r\n\t\tgl.vertexAttribPointer(loc, numElem, gl.FLOAT, false, 0, 0);\r\n\t}\r\n\r\n\tif (indices) {\r\n\t\tconst indexBuffer = gl.createBuffer();\r\n\t\tif (indexBuffer) {\r\n\t\t\tgl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);\r\n\t\t\tgl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);\r\n\t\t}\r\n\t}\r\n\r\n\tgl.bindVertexArray(null);\r\n\treturn va;\r\n}\r\n\r\nfunction resizeCanvasToDisplaySize(canvas: HTMLCanvasElement): boolean {\r\n\tconst dpr = Math.min(2, window.devicePixelRatio || 1);\r\n\tconst displayWidth = Math.round(canvas.clientWidth * dpr);\r\n\tconst displayHeight = Math.round(canvas.clientHeight * dpr);\r\n\tconst needResize =\r\n\t\tcanvas.width !== displayWidth || canvas.height !== displayHeight;\r\n\tif (needResize) {\r\n\t\tcanvas.width = displayWidth;\r\n\t\tcanvas.height = displayHeight;\r\n\t}\r\n\treturn needResize;\r\n}\r\n\r\nfunction makeBuffer(\r\n\tgl: WebGL2RenderingContext,\r\n\tsizeOrData: number | ArrayBufferView,\r\n\tusage: number\r\n): WebGLBuffer {\r\n\tconst buf = gl.createBuffer();\r\n\tif (!buf) {\r\n\t\tthrow new Error(\"Failed to create WebGL buffer.\");\r\n\t}\r\n\tgl.bindBuffer(gl.ARRAY_BUFFER, buf);\r\n\r\n\tif (typeof sizeOrData === \"number\") {\r\n\t\tgl.bufferData(gl.ARRAY_BUFFER, sizeOrData, usage);\r\n\t} else {\r\n\t\tgl.bufferData(gl.ARRAY_BUFFER, sizeOrData, usage);\r\n\t}\r\n\r\n\tgl.bindBuffer(gl.ARRAY_BUFFER, null);\r\n\treturn buf;\r\n}\r\n\r\nfunction createAndSetupTexture(\r\n\tgl: WebGL2RenderingContext,\r\n\tminFilter: number,\r\n\tmagFilter: number,\r\n\twrapS: number,\r\n\twrapT: number\r\n): WebGLTexture {\r\n\tconst texture = gl.createTexture();\r\n\tif (!texture) {\r\n\t\tthrow new Error(\"Failed to create WebGL texture.\");\r\n\t}\r\n\tgl.bindTexture(gl.TEXTURE_2D, texture);\r\n\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS);\r\n\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT);\r\n\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter);\r\n\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter);\r\n\treturn texture;\r\n}\r\n\r\n// -------- Arcball Control --------\r\n\r\ntype UpdateCallback = (deltaTime: number) => void;\r\n\r\nclass ArcballControl {\r\n\tprivate canvas: HTMLCanvasElement;\r\n\tprivate updateCallback: UpdateCallback;\r\n\r\n\tpublic isPointerDown = false;\r\n\tpublic orientation = quat.create();\r\n\tpublic pointerRotation = quat.create();\r\n\tpublic rotationVelocity = 0;\r\n\tpublic rotationAxis = vec3.fromValues(1, 0, 0);\r\n\r\n\tpublic snapDirection = vec3.fromValues(0, 0, -1);\r\n\tpublic snapTargetDirection: vec3 | null = null;\r\n\r\n\tprivate pointerPos = vec2.create();\r\n\tprivate previousPointerPos = vec2.create();\r\n\tprivate _rotationVelocity = 0; // smoother rotational velocity\r\n\tprivate _combinedQuat = quat.create();\r\n\r\n\tprivate readonly EPSILON = 0.1;\r\n\tprivate readonly IDENTITY_QUAT = quat.create();\r\n\r\n\tconstructor(canvas: HTMLCanvasElement, updateCallback?: UpdateCallback) {\r\n\t\tthis.canvas = canvas;\r\n\t\tthis.updateCallback = updateCallback || (() => undefined);\r\n\r\n\t\tcanvas.addEventListener(\"pointerdown\", (e: PointerEvent) => {\r\n\t\t\tvec2.set(this.pointerPos, e.clientX, e.clientY);\r\n\t\t\tvec2.copy(this.previousPointerPos, this.pointerPos);\r\n\t\t\tthis.isPointerDown = true;\r\n\t\t});\r\n\t\tcanvas.addEventListener(\"pointerup\", () => {\r\n\t\t\tthis.isPointerDown = false;\r\n\t\t});\r\n\t\tcanvas.addEventListener(\"pointerleave\", () => {\r\n\t\t\tthis.isPointerDown = false;\r\n\t\t});\r\n\t\tcanvas.addEventListener(\"pointermove\", (e: PointerEvent) => {\r\n\t\t\tif (this.isPointerDown) {\r\n\t\t\t\tvec2.set(this.pointerPos, e.clientX, e.clientY);\r\n\t\t\t}\r\n\t\t});\r\n\r\n\t\t// disable default panning in touch UIs\r\n\t\tcanvas.style.touchAction = \"none\";\r\n\t}\r\n\r\n\tpublic update(deltaTime: number, targetFrameDuration = 16): void {\r\n\t\tconst timeScale = deltaTime / targetFrameDuration + 0.00001;\r\n\t\tlet angleFactor = timeScale;\r\n\t\tconst snapRotation = quat.create();\r\n\r\n\t\tif (this.isPointerDown) {\r\n\t\t\tconst INTENSITY = 0.3 * timeScale;\r\n\t\t\tconst ANGLE_AMPLIFICATION = 5 / timeScale;\r\n\r\n\t\t\t// approximate midpoint for the pointer delta\r\n\t\t\tconst midPointerPos = vec2.sub(\r\n\t\t\t\tvec2.create(),\r\n\t\t\t\tthis.pointerPos,\r\n\t\t\t\tthis.previousPointerPos\r\n\t\t\t);\r\n\t\t\tvec2.scale(midPointerPos, midPointerPos, INTENSITY);\r\n\r\n\t\t\tif (vec2.sqrLen(midPointerPos) > this.EPSILON) {\r\n\t\t\t\tvec2.add(midPointerPos, this.previousPointerPos, midPointerPos);\r\n\r\n\t\t\t\tconst p = this.project(midPointerPos);\r\n\t\t\t\tconst q = this.project(this.previousPointerPos);\r\n\t\t\t\tconst a = vec3.normalize(vec3.create(), p);\r\n\t\t\t\tconst b = vec3.normalize(vec3.create(), q);\r\n\r\n\t\t\t\tvec2.copy(this.previousPointerPos, midPointerPos);\r\n\r\n\t\t\t\tangleFactor *= ANGLE_AMPLIFICATION;\r\n\r\n\t\t\t\tthis.quatFromVectors(a, b, this.pointerRotation, angleFactor);\r\n\t\t\t} else {\r\n\t\t\t\t// smoothly return to identity if minimal movement\r\n\t\t\t\tquat.slerp(\r\n\t\t\t\t\tthis.pointerRotation,\r\n\t\t\t\t\tthis.pointerRotation,\r\n\t\t\t\t\tthis.IDENTITY_QUAT,\r\n\t\t\t\t\tINTENSITY\r\n\t\t\t\t);\r\n\t\t\t}\r\n\t\t} else {\r\n\t\t\t// smoothly de-rotate if the user is not dragging\r\n\t\t\tconst INTENSITY = 0.1 * timeScale;\r\n\t\t\tquat.slerp(\r\n\t\t\t\tthis.pointerRotation,\r\n\t\t\t\tthis.pointerRotation,\r\n\t\t\t\tthis.IDENTITY_QUAT,\r\n\t\t\t\tINTENSITY\r\n\t\t\t);\r\n\r\n\t\t\tif (this.snapTargetDirection) {\r\n\t\t\t\tconst SNAPPING_INTENSITY = 0.2;\r\n\t\t\t\tconst a = this.snapTargetDirection;\r\n\t\t\t\tconst b = this.snapDirection;\r\n\t\t\t\tconst sqrDist = vec3.squaredDistance(a, b);\r\n\t\t\t\tconst distanceFactor = Math.max(0.1, 1 - sqrDist * 10);\r\n\t\t\t\tangleFactor *= SNAPPING_INTENSITY * distanceFactor;\r\n\t\t\t\tthis.quatFromVectors(a, b, snapRotation, angleFactor);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// combine pointer rotation with snap rotation\r\n\t\tconst combinedQuat = quat.multiply(\r\n\t\t\tquat.create(),\r\n\t\t\tsnapRotation,\r\n\t\t\tthis.pointerRotation\r\n\t\t);\r\n\t\tthis.orientation = quat.multiply(\r\n\t\t\tquat.create(),\r\n\t\t\tcombinedQuat,\r\n\t\t\tthis.orientation\r\n\t\t);\r\n\t\tquat.normalize(this.orientation, this.orientation);\r\n\r\n\t\tconst RA_INTENSITY = 0.8 * timeScale;\r\n\t\tquat.slerp(\r\n\t\t\tthis._combinedQuat,\r\n\t\t\tthis._combinedQuat,\r\n\t\t\tcombinedQuat,\r\n\t\t\tRA_INTENSITY\r\n\t\t);\r\n\t\tquat.normalize(this._combinedQuat, this._combinedQuat);\r\n\r\n\t\tconst rad = Math.acos(this._combinedQuat[3]) * 2.0;\r\n\t\tconst s = Math.sin(rad / 2.0);\r\n\t\tlet rv = 0;\r\n\t\tif (s > 0.000001) {\r\n\t\t\trv = rad / (2 * Math.PI);\r\n\t\t\tthis.rotationAxis[0] = this._combinedQuat[0] / s;\r\n\t\t\tthis.rotationAxis[1] = this._combinedQuat[1] / s;\r\n\t\t\tthis.rotationAxis[2] = this._combinedQuat[2] / s;\r\n\t\t}\r\n\r\n\t\tconst RV_INTENSITY = 0.5 * timeScale;\r\n\t\tthis._rotationVelocity += (rv - this._rotationVelocity) * RV_INTENSITY;\r\n\t\tthis.rotationVelocity = this._rotationVelocity / timeScale;\r\n\r\n\t\tthis.updateCallback(deltaTime);\r\n\t}\r\n\r\n\tprivate quatFromVectors(\r\n\t\ta: vec3,\r\n\t\tb: vec3,\r\n\t\tout: quat,\r\n\t\tangleFactor = 1\r\n\t): { q: quat; axis: vec3; angle: number } {\r\n\t\tconst axis = vec3.cross(vec3.create(), a, b);\r\n\t\tvec3.normalize(axis, axis);\r\n\t\tconst d = Math.max(-1, Math.min(1, vec3.dot(a, b)));\r\n\t\tconst angle = Math.acos(d) * angleFactor;\r\n\t\tquat.setAxisAngle(out, axis, angle);\r\n\t\treturn { q: out, axis, angle };\r\n\t}\r\n\r\n\tprivate project(pos: vec2): vec3 {\r\n\t\tconst r = 2;\r\n\t\tconst w = this.canvas.clientWidth;\r\n\t\tconst h = this.canvas.clientHeight;\r\n\t\tconst s = Math.max(w, h) - 1;\r\n\r\n\t\t// map to [-1, 1]\r\n\t\tconst x = (2 * pos[0] - w - 1) / s;\r\n\t\tconst y = (2 * pos[1] - h - 1) / s;\r\n\t\tlet z = 0;\r\n\t\tconst xySq = x * x + y * y;\r\n\t\tconst rSq = r * r;\r\n\r\n\t\tif (xySq <= rSq / 2.0) {\r\n\t\t\tz = Math.sqrt(rSq - xySq);\r\n\t\t} else {\r\n\t\t\tz = rSq / Math.sqrt(xySq);\r\n\t\t}\r\n\t\t// note the negative x to make it a bit more intuitive (drag right to rotate right, etc.)\r\n\t\treturn vec3.fromValues(-x, y, z);\r\n\t}\r\n}\r\n\r\n// -------- InfiniteGridMenu --------\r\n\r\ninterface MenuItem {\r\n\timage: string;\r\n\tlink: string;\r\n\ttitle: string;\r\n\tdescription: string;\r\n}\r\n\r\ntype ActiveItemCallback = (index: number) => void;\r\ntype MovementChangeCallback = (isMoving: boolean) => void;\r\ntype InitCallback = (instance: InfiniteGridMenu) => void;\r\n\r\ninterface Camera {\r\n\tmatrix: mat4;\r\n\tnear: number;\r\n\tfar: number;\r\n\tfov: number;\r\n\taspect: number;\r\n\tposition: vec3;\r\n\tup: vec3;\r\n\tmatrices: {\r\n\t\tview: mat4;\r\n\t\tprojection: mat4;\r\n\t\tinversProjection: mat4;\r\n\t};\r\n}\r\n\r\nclass InfiniteGridMenu {\r\n\tprivate gl: WebGL2RenderingContext | null = null;\r\n\tprivate discProgram: WebGLProgram | null = null;\r\n\tprivate discVAO: WebGLVertexArrayObject | null = null;\r\n\tprivate discBuffers!: {\r\n\t\tvertices: Float32Array;\r\n\t\tindices: Uint16Array;\r\n\t\tnormals: Float32Array;\r\n\t\tuvs: Float32Array;\r\n\t};\r\n\tprivate icoGeo!: IcosahedronGeometry;\r\n\tprivate discGeo!: DiscGeometry;\r\n\tprivate worldMatrix = mat4.create();\r\n\tprivate tex: WebGLTexture | null = null;\r\n\tprivate control!: ArcballControl;\r\n\r\n\tprivate discLocations!: {\r\n\t\taModelPosition: number;\r\n\t\taModelUvs: number;\r\n\t\taInstanceMatrix: number;\r\n\t\tuWorldMatrix: WebGLUniformLocation | null;\r\n\t\tuViewMatrix: WebGLUniformLocation | null;\r\n\t\tuProjectionMatrix: WebGLUniformLocation | null;\r\n\t\tuCameraPosition: WebGLUniformLocation | null;\r\n\t\tuScaleFactor: WebGLUniformLocation | null;\r\n\t\tuRotationAxisVelocity: WebGLUniformLocation | null;\r\n\t\tuTex: WebGLUniformLocation | null;\r\n\t\tuFrames: WebGLUniformLocation | null;\r\n\t\tuItemCount: WebGLUniformLocation | null;\r\n\t\tuAtlasSize: WebGLUniformLocation | null;\r\n\t};\r\n\r\n\tprivate viewportSize = vec2.create();\r\n\tprivate drawBufferSize = vec2.create();\r\n\r\n\tprivate discInstances!: {\r\n\t\tmatricesArray: Float32Array;\r\n\t\tmatrices: Float32Array[];\r\n\t\tbuffer: WebGLBuffer | null;\r\n\t};\r\n\r\n\tprivate instancePositions: vec3[] = [];\r\n\tprivate DISC_INSTANCE_COUNT = 0;\r\n\tprivate atlasSize = 1;\r\n\r\n\tprivate _time = 0;\r\n\tprivate _deltaTime = 0;\r\n\tprivate _deltaFrames = 0;\r\n\tprivate _frames = 0;\r\n\r\n\tprivate movementActive = false;\r\n\r\n\tprivate TARGET_FRAME_DURATION = 1000 / 60; // 60 fps\r\n\tprivate SPHERE_RADIUS = 2;\r\n\r\n\tpublic camera: Camera = {\r\n\t\tmatrix: mat4.create(),\r\n\t\tnear: 0.1,\r\n\t\tfar: 40,\r\n\t\tfov: Math.PI / 4,\r\n\t\taspect: 1,\r\n\t\tposition: vec3.fromValues(0, 0, 3),\r\n\t\tup: vec3.fromValues(0, 1, 0),\r\n\t\tmatrices: {\r\n\t\t\tview: mat4.create(),\r\n\t\t\tprojection: mat4.create(),\r\n\t\t\tinversProjection: mat4.create(),\r\n\t\t},\r\n\t};\r\n\r\n\tpublic smoothRotationVelocity = 0;\r\n\tpublic scaleFactor = 1.0;\r\n\r\n\tconstructor(\r\n\t\tprivate canvas: HTMLCanvasElement,\r\n\t\tprivate items: MenuItem[],\r\n\t\tprivate onActiveItemChange: ActiveItemCallback,\r\n\t\tprivate onMovementChange: MovementChangeCallback,\r\n\t\tonInit?: InitCallback\r\n\t) {\r\n\t\tthis.init(onInit);\r\n\t}\r\n\r\n\tpublic resize(): void {\r\n\t\tconst needsResize = resizeCanvasToDisplaySize(this.canvas);\r\n\t\tif (!this.gl) return;\r\n\t\tif (needsResize) {\r\n\t\t\tthis.gl.viewport(\r\n\t\t\t\t0,\r\n\t\t\t\t0,\r\n\t\t\t\tthis.gl.drawingBufferWidth,\r\n\t\t\t\tthis.gl.drawingBufferHeight\r\n\t\t\t);\r\n\t\t}\r\n\t\tthis.updateProjectionMatrix();\r\n\t}\r\n\r\n\tpublic run(time = 0): void {\r\n\t\tthis._deltaTime = Math.min(32, time - this._time);\r\n\t\tthis._time = time;\r\n\t\tthis._deltaFrames = this._deltaTime / this.TARGET_FRAME_DURATION;\r\n\t\tthis._frames += this._deltaFrames;\r\n\r\n\t\tthis.animate(this._deltaTime);\r\n\t\tthis.render();\r\n\r\n\t\trequestAnimationFrame((t) => this.run(t));\r\n\t}\r\n\r\n\tprivate init(onInit?: InitCallback): void {\r\n\t\tconst gl = this.canvas.getContext(\"webgl2\", {\r\n\t\t\tantialias: true,\r\n\t\t\talpha: false,\r\n\t\t});\r\n\t\tif (!gl) {\r\n\t\t\tthrow new Error(\"No WebGL 2 context!\");\r\n\t\t}\r\n\t\tthis.gl = gl;\r\n\r\n\t\tvec2.set(\r\n\t\t\tthis.viewportSize,\r\n\t\t\tthis.canvas.clientWidth,\r\n\t\t\tthis.canvas.clientHeight\r\n\t\t);\r\n\t\tvec2.clone(this.drawBufferSize);\r\n\r\n\t\tthis.discProgram = createProgram(\r\n\t\t\tgl,\r\n\t\t\t[discVertShaderSource, discFragShaderSource],\r\n\t\t\tnull,\r\n\t\t\t{\r\n\t\t\t\taModelPosition: 0,\r\n\t\t\t\taModelNormal: 1, // not used in the code, but let's keep the location\r\n\t\t\t\taModelUvs: 2,\r\n\t\t\t\taInstanceMatrix: 3,\r\n\t\t\t}\r\n\t\t);\r\n\r\n\t\tthis.discLocations = {\r\n\t\t\taModelPosition: gl.getAttribLocation(\r\n\t\t\t\tthis.discProgram!,\r\n\t\t\t\t\"aModelPosition\"\r\n\t\t\t),\r\n\t\t\taModelUvs: gl.getAttribLocation(this.discProgram!, \"aModelUvs\"),\r\n\t\t\taInstanceMatrix: gl.getAttribLocation(\r\n\t\t\t\tthis.discProgram!,\r\n\t\t\t\t\"aInstanceMatrix\"\r\n\t\t\t),\r\n\t\t\tuWorldMatrix: gl.getUniformLocation(this.discProgram!, \"uWorldMatrix\"),\r\n\t\t\tuViewMatrix: gl.getUniformLocation(this.discProgram!, \"uViewMatrix\"),\r\n\t\t\tuProjectionMatrix: gl.getUniformLocation(\r\n\t\t\t\tthis.discProgram!,\r\n\t\t\t\t\"uProjectionMatrix\"\r\n\t\t\t),\r\n\t\t\tuCameraPosition: gl.getUniformLocation(\r\n\t\t\t\tthis.discProgram!,\r\n\t\t\t\t\"uCameraPosition\"\r\n\t\t\t),\r\n\t\t\tuScaleFactor: gl.getUniformLocation(this.discProgram!, \"uScaleFactor\"),\r\n\t\t\tuRotationAxisVelocity: gl.getUniformLocation(\r\n\t\t\t\tthis.discProgram!,\r\n\t\t\t\t\"uRotationAxisVelocity\"\r\n\t\t\t),\r\n\t\t\tuTex: gl.getUniformLocation(this.discProgram!, \"uTex\"),\r\n\t\t\tuFrames: gl.getUniformLocation(this.discProgram!, \"uFrames\"),\r\n\t\t\tuItemCount: gl.getUniformLocation(this.discProgram!, \"uItemCount\"),\r\n\t\t\tuAtlasSize: gl.getUniformLocation(this.discProgram!, \"uAtlasSize\"),\r\n\t\t};\r\n\r\n\t\t// Geometry\r\n\t\tthis.discGeo = new DiscGeometry(56, 1);\r\n\t\tthis.discBuffers = this.discGeo.data;\r\n\t\tthis.discVAO = makeVertexArray(\r\n\t\t\tgl,\r\n\t\t\t[\r\n\t\t\t\t[\r\n\t\t\t\t\tmakeBuffer(gl, this.discBuffers.vertices, gl.STATIC_DRAW),\r\n\t\t\t\t\tthis.discLocations.aModelPosition,\r\n\t\t\t\t\t3,\r\n\t\t\t\t],\r\n\t\t\t\t[\r\n\t\t\t\t\tmakeBuffer(gl, this.discBuffers.uvs, gl.STATIC_DRAW),\r\n\t\t\t\t\tthis.discLocations.aModelUvs,\r\n\t\t\t\t\t2,\r\n\t\t\t\t],\r\n\t\t\t],\r\n\t\t\tthis.discBuffers.indices\r\n\t\t);\r\n\r\n\t\tthis.icoGeo = new IcosahedronGeometry();\r\n\t\tthis.icoGeo.subdivide(1).spherize(this.SPHERE_RADIUS);\r\n\t\tthis.instancePositions = this.icoGeo.vertices.map((v) => v.position);\r\n\t\tthis.DISC_INSTANCE_COUNT = this.icoGeo.vertices.length;\r\n\t\tthis.initDiscInstances(this.DISC_INSTANCE_COUNT);\r\n\r\n\t\t// Texture\r\n\t\tthis.initTexture();\r\n\r\n\t\t// Arcball\r\n\t\tthis.control = new ArcballControl(this.canvas, (deltaTime) =>\r\n\t\t\tthis.onControlUpdate(deltaTime)\r\n\t\t);\r\n\r\n\t\tthis.updateCameraMatrix();\r\n\t\tthis.updateProjectionMatrix();\r\n\r\n\t\t// Ensure correct size on first load\r\n\t\tthis.resize();\r\n\r\n\t\tif (onInit) {\r\n\t\t\tonInit(this);\r\n\t\t}\r\n\t}\r\n\r\n\tprivate initTexture(): void {\r\n\t\tif (!this.gl) return;\r\n\t\tconst gl = this.gl;\r\n\t\tthis.tex = createAndSetupTexture(\r\n\t\t\tgl,\r\n\t\t\tgl.LINEAR,\r\n\t\t\tgl.LINEAR,\r\n\t\t\tgl.CLAMP_TO_EDGE,\r\n\t\t\tgl.CLAMP_TO_EDGE\r\n\t\t);\r\n\r\n\t\tconst itemCount = Math.max(1, this.items.length);\r\n\t\tthis.atlasSize = Math.ceil(Math.sqrt(itemCount));\r\n\t\tconst cellSize = 512;\r\n\t\tconst canvas = document.createElement(\"canvas\");\r\n\t\tconst ctx = canvas.getContext(\"2d\")!;\r\n\t\tcanvas.width = this.atlasSize * cellSize;\r\n\t\tcanvas.height = this.atlasSize * cellSize;\r\n\r\n\t\tPromise.all(\r\n\t\t\tthis.items.map(\r\n\t\t\t\t(item) =>\r\n\t\t\t\t\tnew Promise<HTMLImageElement>((resolve) => {\r\n\t\t\t\t\t\tconst img = new Image();\r\n\t\t\t\t\t\timg.crossOrigin = \"anonymous\";\r\n\t\t\t\t\t\timg.onload = () => resolve(img);\r\n\t\t\t\t\t\timg.src = item.image;\r\n\t\t\t\t\t})\r\n\t\t\t)\r\n\t\t).then((images) => {\r\n\t\t\timages?.forEach((img, i) => {\r\n\t\t\t\tconst x = (i % this.atlasSize) * cellSize;\r\n\t\t\t\tconst y = Math.floor(i / this.atlasSize) * cellSize;\r\n\t\t\t\tctx.drawImage(img, x, y, cellSize, cellSize);\r\n\t\t\t});\r\n\r\n\t\t\tgl.bindTexture(gl.TEXTURE_2D, this.tex);\r\n\t\t\tgl.texImage2D(\r\n\t\t\t\tgl.TEXTURE_2D,\r\n\t\t\t\t0,\r\n\t\t\t\tgl.RGBA,\r\n\t\t\t\tgl.RGBA,\r\n\t\t\t\tgl.UNSIGNED_BYTE,\r\n\t\t\t\tcanvas\r\n\t\t\t);\r\n\t\t\tgl.generateMipmap(gl.TEXTURE_2D);\r\n\t\t});\r\n\t}\r\n\r\n\tprivate initDiscInstances(count: number): void {\r\n\t\tif (!this.gl || !this.discVAO) return;\r\n\t\tconst gl = this.gl;\r\n\r\n\t\tconst matricesArray = new Float32Array(count * 16);\r\n\t\tconst matrices: Float32Array[] = [];\r\n\t\tfor (let i = 0; i < count; ++i) {\r\n\t\t\tconst instanceMatrixArray = new Float32Array(\r\n\t\t\t\tmatricesArray.buffer,\r\n\t\t\t\ti * 16 * 4,\r\n\t\t\t\t16\r\n\t\t\t);\r\n\t\t\tmat4.identity(instanceMatrixArray as unknown as mat4);\r\n\t\t\tmatrices.push(instanceMatrixArray);\r\n\t\t}\r\n\r\n\t\tthis.discInstances = {\r\n\t\t\tmatricesArray,\r\n\t\t\tmatrices,\r\n\t\t\tbuffer: gl.createBuffer(),\r\n\t\t};\r\n\r\n\t\tgl.bindVertexArray(this.discVAO);\r\n\t\tgl.bindBuffer(gl.ARRAY_BUFFER, this.discInstances.buffer);\r\n\t\tgl.bufferData(\r\n\t\t\tgl.ARRAY_BUFFER,\r\n\t\t\tthis.discInstances.matricesArray.byteLength,\r\n\t\t\tgl.DYNAMIC_DRAW\r\n\t\t);\r\n\r\n\t\tconst mat4AttribSlotCount = 4;\r\n\t\tconst bytesPerMatrix = 16 * 4; // 16 floats, 4 bytes each\r\n\t\tfor (let j = 0; j < mat4AttribSlotCount; ++j) {\r\n\t\t\tconst loc = this.discLocations.aInstanceMatrix + j;\r\n\t\t\tgl.enableVertexAttribArray(loc);\r\n\t\t\tgl.vertexAttribPointer(\r\n\t\t\t\tloc,\r\n\t\t\t\t4,\r\n\t\t\t\tgl.FLOAT,\r\n\t\t\t\tfalse,\r\n\t\t\t\tbytesPerMatrix,\r\n\t\t\t\tj * 4 * 4\r\n\t\t\t);\r\n\t\t\tgl.vertexAttribDivisor(loc, 1);\r\n\t\t}\r\n\t\tgl.bindBuffer(gl.ARRAY_BUFFER, null);\r\n\t\tgl.bindVertexArray(null);\r\n\t}\r\n\r\n\tprivate animate(deltaTime: number): void {\r\n\t\tif (!this.gl) return;\r\n\t\tthis.control.update(deltaTime, this.TARGET_FRAME_DURATION);\r\n\r\n\t\tconst positions = this.instancePositions.map((p) =>\r\n\t\t\tvec3.transformQuat(vec3.create(), p, this.control.orientation)\r\n\t\t);\r\n\t\tconst scale = 0.25;\r\n\t\tconst SCALE_INTENSITY = 0.6;\r\n\r\n\t\tpositions.forEach((p, ndx) => {\r\n\t\t\tconst s =\r\n\t\t\t\t(Math.abs(p[2]) / this.SPHERE_RADIUS) * SCALE_INTENSITY +\r\n\t\t\t\t(1 - SCALE_INTENSITY);\r\n\t\t\tconst finalScale = s * scale;\r\n\t\t\tconst matrix = mat4.create();\r\n\r\n\t\t\t// translate disc so it faces outward\r\n\t\t\tmat4.multiply(\r\n\t\t\t\tmatrix,\r\n\t\t\t\tmatrix,\r\n\t\t\t\tmat4.fromTranslation(mat4.create(), vec3.negate(vec3.create(), p))\r\n\t\t\t);\r\n\t\t\tmat4.multiply(\r\n\t\t\t\tmatrix,\r\n\t\t\t\tmatrix,\r\n\t\t\t\tmat4.targetTo(mat4.create(), [0, 0, 0], p, [0, 1, 0])\r\n\t\t\t);\r\n\t\t\tmat4.multiply(\r\n\t\t\t\tmatrix,\r\n\t\t\t\tmatrix,\r\n\t\t\t\tmat4.fromScaling(mat4.create(), [\r\n\t\t\t\t\tfinalScale,\r\n\t\t\t\t\tfinalScale,\r\n\t\t\t\t\tfinalScale,\r\n\t\t\t\t])\r\n\t\t\t);\r\n\t\t\tmat4.multiply(\r\n\t\t\t\tmatrix,\r\n\t\t\t\tmatrix,\r\n\t\t\t\tmat4.fromTranslation(mat4.create(), [0, 0, -this.SPHERE_RADIUS])\r\n\t\t\t);\r\n\r\n\t\t\tmat4.copy(this.discInstances.matrices[ndx], matrix);\r\n\t\t});\r\n\r\n\t\t// Update instance buffer\r\n\t\tthis.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.discInstances.buffer);\r\n\t\tthis.gl.bufferSubData(\r\n\t\t\tthis.gl.ARRAY_BUFFER,\r\n\t\t\t0,\r\n\t\t\tthis.discInstances.matricesArray\r\n\t\t);\r\n\t\tthis.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);\r\n\r\n\t\tthis.smoothRotationVelocity = this.control.rotationVelocity;\r\n\t}\r\n\r\n\tprivate render(): void {\r\n\t\tif (!this.gl || !this.discProgram) return;\r\n\t\tconst gl = this.gl;\r\n\r\n\t\tgl.useProgram(this.discProgram);\r\n\t\tgl.enable(gl.CULL_FACE);\r\n\t\tgl.enable(gl.DEPTH_TEST);\r\n\r\n\t\tgl.clearColor(0, 0, 0, 0);\r\n\t\tgl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);\r\n\r\n\t\tgl.uniformMatrix4fv(\r\n\t\t\tthis.discLocations.uWorldMatrix,\r\n\t\t\tfalse,\r\n\t\t\tthis.worldMatrix\r\n\t\t);\r\n\t\tgl.uniformMatrix4fv(\r\n\t\t\tthis.discLocations.uViewMatrix,\r\n\t\t\tfalse,\r\n\t\t\tthis.camera.matrices.view\r\n\t\t);\r\n\t\tgl.uniformMatrix4fv(\r\n\t\t\tthis.discLocations.uProjectionMatrix,\r\n\t\t\tfalse,\r\n\t\t\tthis.camera.matrices.projection\r\n\t\t);\r\n\t\tgl.uniform3f(\r\n\t\t\tthis.discLocations.uCameraPosition,\r\n\t\t\tthis.camera.position[0],\r\n\t\t\tthis.camera.position[1],\r\n\t\t\tthis.camera.position[2]\r\n\t\t);\r\n\t\tgl.uniform4f(\r\n\t\t\tthis.discLocations.uRotationAxisVelocity,\r\n\t\t\tthis.control.rotationAxis[0],\r\n\t\t\tthis.control.rotationAxis[1],\r\n\t\t\tthis.control.rotationAxis[2],\r\n\t\t\tthis.smoothRotationVelocity * 1.1\r\n\t\t);\r\n\r\n\t\tgl.uniform1i(this.discLocations.uItemCount, this.items.length);\r\n\t\tgl.uniform1i(this.discLocations.uAtlasSize, this.atlasSize);\r\n\r\n\t\tgl.uniform1f(this.discLocations.uFrames, this._frames);\r\n\t\tgl.uniform1f(this.discLocations.uScaleFactor, this.scaleFactor);\r\n\r\n\t\tgl.uniform1i(this.discLocations.uTex, 0);\r\n\t\tgl.activeTexture(gl.TEXTURE0);\r\n\t\tgl.bindTexture(gl.TEXTURE_2D, this.tex);\r\n\r\n\t\tgl.bindVertexArray(this.discVAO);\r\n\t\tgl.drawElementsInstanced(\r\n\t\t\tgl.TRIANGLES,\r\n\t\t\tthis.discBuffers.indices.length,\r\n\t\t\tgl.UNSIGNED_SHORT,\r\n\t\t\t0,\r\n\t\t\tthis.DISC_INSTANCE_COUNT\r\n\t\t);\r\n\t\tgl.bindVertexArray(null);\r\n\t}\r\n\r\n\tprivate updateCameraMatrix(): void {\r\n\t\tmat4.targetTo(\r\n\t\t\tthis.camera.matrix,\r\n\t\t\tthis.camera.position,\r\n\t\t\t[0, 0, 0],\r\n\t\t\tthis.camera.up\r\n\t\t);\r\n\t\tmat4.invert(this.camera.matrices.view, this.camera.matrix);\r\n\t}\r\n\r\n\tprivate updateProjectionMatrix(): void {\r\n\t\tif (!this.gl) return;\r\n\t\tconst canvasEl = this.gl.canvas as HTMLCanvasElement;\r\n\t\tthis.camera.aspect = canvasEl.clientWidth / canvasEl.clientHeight;\r\n\t\tconst height = this.SPHERE_RADIUS * 0.35;\r\n\t\tconst distance = this.camera.position[2];\r\n\t\tif (this.camera.aspect > 1) {\r\n\t\t\tthis.camera.fov = 2 * Math.atan(height / distance);\r\n\t\t} else {\r\n\t\t\tthis.camera.fov =\r\n\t\t\t\t2 * Math.atan(height / this.camera.aspect / distance);\r\n\t\t}\r\n\t\tmat4.perspective(\r\n\t\t\tthis.camera.matrices.projection,\r\n\t\t\tthis.camera.fov,\r\n\t\t\tthis.camera.aspect,\r\n\t\t\tthis.camera.near,\r\n\t\t\tthis.camera.far\r\n\t\t);\r\n\t\tmat4.invert(\r\n\t\t\tthis.camera.matrices.inversProjection,\r\n\t\t\tthis.camera.matrices.projection\r\n\t\t);\r\n\t}\r\n\r\n\tprivate onControlUpdate(deltaTime: number): void {\r\n\t\tconst timeScale = deltaTime / this.TARGET_FRAME_DURATION + 0.0001;\r\n\t\tlet damping = 5 / timeScale;\r\n\t\tlet cameraTargetZ = 3;\r\n\r\n\t\tconst isMoving =\r\n\t\t\tthis.control.isPointerDown ||\r\n\t\t\tMath.abs(this.smoothRotationVelocity) > 0.01;\r\n\r\n\t\tif (isMoving !== this.movementActive) {\r\n\t\t\tthis.movementActive = isMoving;\r\n\t\t\tthis.onMovementChange(isMoving);\r\n\t\t}\r\n\r\n\t\t// handle snapping to nearest item if not dragging\r\n\t\tif (!this.control.isPointerDown) {\r\n\t\t\tconst nearestVertexIndex = this.findNearestVertexIndex();\r\n\t\t\tconst itemIndex = nearestVertexIndex % Math.max(1, this.items.length);\r\n\t\t\tthis.onActiveItemChange(itemIndex);\r\n\t\t\tconst snapDirection = vec3.normalize(\r\n\t\t\t\tvec3.create(),\r\n\t\t\t\tthis.getVertexWorldPosition(nearestVertexIndex)\r\n\t\t\t);\r\n\t\t\tthis.control.snapTargetDirection = snapDirection;\r\n\t\t} else {\r\n\t\t\t// push camera back if user is dragging quickly\r\n\t\t\tcameraTargetZ += this.control.rotationVelocity * 80 + 2.5;\r\n\t\t\tdamping = 7 / timeScale;\r\n\t\t}\r\n\r\n\t\tthis.camera.position[2] +=\r\n\t\t\t(cameraTargetZ - this.camera.position[2]) / damping;\r\n\t\tthis.updateCameraMatrix();\r\n\t}\r\n\r\n\tprivate findNearestVertexIndex(): number {\r\n\t\tconst n = this.control.snapDirection;\r\n\t\tconst inversOrientation = quat.conjugate(\r\n\t\t\tquat.create(),\r\n\t\t\tthis.control.orientation\r\n\t\t);\r\n\t\tconst nt = vec3.transformQuat(vec3.create(), n, inversOrientation);\r\n\r\n\t\tlet maxD = -1;\r\n\t\tlet nearestVertexIndex = 0;\r\n\t\tfor (let i = 0; i < this.instancePositions.length; ++i) {\r\n\t\t\tconst d = vec3.dot(nt, this.instancePositions[i]);\r\n\t\t\tif (d > maxD) {\r\n\t\t\t\tmaxD = d;\r\n\t\t\t\tnearestVertexIndex = i;\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn nearestVertexIndex;\r\n\t}\r\n\r\n\tprivate getVertexWorldPosition(index: number): vec3 {\r\n\t\tconst nearestVertexPos = this.instancePositions[index];\r\n\t\treturn vec3.transformQuat(\r\n\t\t\tvec3.create(),\r\n\t\t\tnearestVertexPos,\r\n\t\t\tthis.control.orientation\r\n\t\t);\r\n\t}\r\n}\r\n\r\n// -------- Default Items --------\r\n\r\nconst defaultItems: MenuItem[] = [\r\n\t{\r\n\t\timage: \"https://picsum.photos/900/900?grayscale\",\r\n\t\tlink: \"https://google.com/\",\r\n\t\ttitle: \"\",\r\n\t\tdescription: \"\",\r\n\t},\r\n];\r\n\r\n// -------- React Component --------\r\n\r\ninterface InfiniteMenuProps {\r\n\titems?: MenuItem[];\r\n}\r\n\r\nconst InfiniteMenu: FC<InfiniteMenuProps> = ({ items = [] }) => {\r\n\tconst canvasRef = useRef<HTMLCanvasElement | null>(\r\n\t\tnull\r\n\t) as MutableRefObject<HTMLCanvasElement | null>;\r\n\tconst [activeItem, setActiveItem] = useState<MenuItem | null>(null);\r\n\tconst [isMoving, setIsMoving] = useState<boolean>(false);\r\n\r\n\tuseEffect(() => {\r\n\t\tconst canvas = canvasRef.current;\r\n\t\tlet sketch: InfiniteGridMenu | null = null;\r\n\r\n\t\tconst handleActiveItem = (index: number) => {\r\n\t\t\tif (!items.length) return;\r\n\t\t\tconst itemIndex = index % items.length;\r\n\t\t\tsetActiveItem(items[itemIndex]);\r\n\t\t};\r\n\r\n\t\tif (canvas) {\r\n\t\t\tsketch = new InfiniteGridMenu(\r\n\t\t\t\tcanvas,\r\n\t\t\t\titems.length ? items : defaultItems,\r\n\t\t\t\thandleActiveItem,\r\n\t\t\t\tsetIsMoving,\r\n\t\t\t\t(sk) => sk.run()\r\n\t\t\t);\r\n\t\t}\r\n\r\n\t\tconst handleResize = () => {\r\n\t\t\tif (sketch) {\r\n\t\t\t\tsketch.resize();\r\n\t\t\t}\r\n\t\t};\r\n\r\n\t\twindow.addEventListener(\"resize\", handleResize);\r\n\t\thandleResize();\r\n\r\n\t\treturn () => {\r\n\t\t\twindow.removeEventListener(\"resize\", handleResize);\r\n\t\t};\r\n\t}, [items]);\r\n\r\n\tconst handleButtonClick = () => {\r\n\t\tif (!activeItem?.link) return;\r\n\t\tif (activeItem.link.startsWith(\"http\")) {\r\n\t\t\twindow.open(activeItem.link, \"_blank\");\r\n\t\t} else {\r\n\t\t\t// internal route logic here\r\n\t\t}\r\n\t};\r\n\r\n\treturn (\r\n\t\t<div className=\"relative w-full h-full\">\r\n\t\t\t<canvas\r\n\t\t\t\tid=\"infinite-grid-menu-canvas\"\r\n\t\t\t\tref={canvasRef}\r\n\t\t\t\tclassName=\"cursor-grab w-full h-full overflow-hidden relative outline-hidden active:cursor-grabbing\"\r\n\t\t\t/>\r\n\r\n\t\t\t{activeItem && (\r\n\t\t\t\t<>\r\n\t\t\t\t\t{/* Title */}\r\n\t\t\t\t\t<h2\r\n\t\t\t\t\t\tclassName={`\r\n          select-none\r\n          absolute\r\n          font-black\r\n          text-[4rem]\r\n          left-[1.6em]\r\n          top-1/2\r\n          transform\r\n          translate-x-[20%]\r\n          -translate-y-1/2\r\n          transition-opacity\r\n          ease-[cubic-bezier(0.25,0.1,0.25,1.0)]\r\n          ${\r\n\t\t\t\t\tisMoving\r\n\t\t\t\t\t\t? \"opacity-0 pointer-events-none duration-100\"\r\n\t\t\t\t\t\t: \"opacity-100 pointer-events-auto duration-500\"\r\n\t\t\t\t}\r\n        `}\r\n\t\t\t\t\t>\r\n\t\t\t\t\t\t{activeItem.title}\r\n\t\t\t\t\t</h2>\r\n\r\n\t\t\t\t\t{/* Description */}\r\n\t\t\t\t\t<p\r\n\t\t\t\t\t\tclassName={`\r\n          select-none\r\n          absolute\r\n          max-w-[10ch]\r\n          text-[1.5rem]\r\n          top-1/2\r\n          right-[1%]\r\n          transition-[transform,opacity]\r\n          ease-[cubic-bezier(0.25,0.1,0.25,1.0)]\r\n          ${\r\n\t\t\t\t\tisMoving\r\n\t\t\t\t\t\t? \"opacity-0 pointer-events-none duration-100 translate-x-[-60%] -translate-y-1/2\"\r\n\t\t\t\t\t\t: \"opacity-100 pointer-events-auto duration-500 translate-x-[-90%] -translate-y-1/2\"\r\n\t\t\t\t}\r\n        `}\r\n\t\t\t\t\t>\r\n\t\t\t\t\t\t{activeItem.description}\r\n\t\t\t\t\t</p>\r\n\r\n\t\t\t\t\t{/* Action Button */}\r\n\t\t\t\t\t<div\r\n\t\t\t\t\t\tonClick={handleButtonClick}\r\n\t\t\t\t\t\tclassName={`\r\n          absolute\r\n          left-1/2\r\n          z-10\r\n          w-[60px]\r\n          h-[60px]\r\n          grid\r\n          place-items-center\r\n          bg-background\r\n          border-[5px]\r\n          border-black\r\n          rounded-full\r\n          cursor-pointer\r\n          transition-[bottom,opacity,transform]\r\n          ease-[cubic-bezier(0.25,0.1,0.25,1.0)]\r\n          ${\r\n\t\t\t\t\tisMoving\r\n\t\t\t\t\t\t? \"bottom-[-80px] opacity-0 pointer-events-none duration-100 scale-0 -translate-x-1/2\"\r\n\t\t\t\t\t\t: \"bottom-[3.8em] opacity-100 pointer-events-auto duration-500 scale-100 -translate-x-1/2\"\r\n\t\t\t\t}\r\n        `}\r\n\t\t\t\t\t>\r\n\t\t\t\t\t\t<p className=\"select-none relative text-foreground top-[2px] text-[26px]\">\r\n\t\t\t\t\t\t\t&#x2197;\r\n\t\t\t\t\t\t</p>\r\n\t\t\t\t\t</div>\r\n\t\t\t\t</>\r\n\t\t\t)}\r\n\t\t</div>\r\n\t);\r\n};\r\n\r\nexport default InfiniteMenu;\r\n",
      "type": "registry:ui"
    }
  ]
}