import * as THREE from "three"; import { ACTIVE_SYSTEM_DETAIL_SCALE, MOON_RENDER_SCALE, PLANET_RENDER_SCALE, STAR_RENDER_SCALE, } from "./viewerConstants"; import type { CelestialSnapshot, ClaimSnapshot, ConstructionSiteSnapshot, MoonSnapshot, PlanetSnapshot, ResourceDepositSnapshot, ResourceNodeSnapshot, ShipSnapshot, StationSnapshot, SystemSnapshot, } from "./contracts"; import type { MoonVisual } from "./viewerTypes"; import { celestialRenderRadius, computeMoonLocalPosition, computeMoonRenderRadius, computePlanetLocalPosition, scaleLocalVector, starHaloOpacity, toThreeVector, } from "./viewerMath"; import { createSceneNode } from "./viewerScenePrimitives"; import type { SceneNode } from "./viewerScenePrimitives"; export function createNodeMesh(node: ResourceNodeSnapshot): SceneNode { const isGas = node.sourceKind === "gas-cloud" || node.itemId === "gas"; const mesh = new THREE.Mesh( isGas ? new THREE.SphereGeometry(18, 14, 14) : new THREE.IcosahedronGeometry(12, 0), new THREE.MeshStandardMaterial({ color: isGas ? 0x7fd6ff : 0xd2b07a, flatShading: !isGas, transparent: isGas, opacity: isGas ? 0.68 : 1, emissive: new THREE.Color(isGas ? 0x7fd6ff : 0xd2b07a).multiplyScalar(isGas ? 0.22 : 0.05), }), ); mesh.position.copy(toThreeVector(node.localPosition)); mesh.scale.setScalar(0.8 + (node.oreRemaining / Math.max(node.maxOre, 1)) * 0.6); return createSceneNode(mesh); } export function createResourceDepositMesh(deposit: ResourceDepositSnapshot, node: ResourceNodeSnapshot): SceneNode { const isGas = node.sourceKind === "gas-cloud" || node.itemId === "gas"; const oreRatio = deposit.maxOre <= 0.01 ? 0 : deposit.oreRemaining / deposit.maxOre; const mesh = new THREE.Mesh( isGas ? new THREE.SphereGeometry(10, 12, 12) : new THREE.DodecahedronGeometry(8 + (oreRatio * 5), 0), new THREE.MeshStandardMaterial({ color: isGas ? 0x7fd6ff : 0xc9a165, flatShading: !isGas, transparent: isGas, opacity: isGas ? 0.55 : 1, emissive: new THREE.Color(isGas ? 0x7fd6ff : 0xc9a165).multiplyScalar(isGas ? 0.18 : 0.05), }), ); mesh.position.copy(toThreeVector(deposit.localPosition)); return createSceneNode(mesh); } export function createCelestialMesh(node: CelestialSnapshot, celestialColor: (kind: string) => string): SceneNode { const color = celestialColor(node.kind); return createSceneNode(new THREE.Mesh( new THREE.OctahedronGeometry(0.08, 0), new THREE.MeshStandardMaterial({ color, emissive: new THREE.Color(color).multiplyScalar(0.16), roughness: 0.35, metalness: 0.45, }), )); } export function createClaimMesh(claim: ClaimSnapshot): SceneNode { return createSceneNode(new THREE.Mesh( new THREE.ConeGeometry(9, 20, 4), new THREE.MeshStandardMaterial({ color: claim.state === "active" ? 0xff7f50 : 0xff5b5b, emissive: new THREE.Color(claim.state === "active" ? 0xff7f50 : 0xff5b5b).multiplyScalar(0.16), roughness: 0.4, metalness: 0.28, }), )); } export function createConstructionSiteMesh(site: ConstructionSiteSnapshot): SceneNode { return createSceneNode(new THREE.Mesh( new THREE.TorusKnotGeometry(7, 2.2, 54, 8), new THREE.MeshStandardMaterial({ color: site.state === "completed" ? 0x46d37f : 0x9df29c, emissive: new THREE.Color(site.state === "completed" ? 0x46d37f : 0x9df29c).multiplyScalar(0.15), roughness: 0.34, metalness: 0.48, }), )); } export function createStarCluster(system: SystemSnapshot): SceneNode { const root = new THREE.Group(); for (const [index, star] of system.stars.entries()) { const renderedSize = celestialRenderRadius(star.size, 0.00018, 40, 0.62); const offset = system.stars.length > 1 ? (index === 0 ? new THREE.Vector3(-renderedSize * 0.55, 0, 0) : new THREE.Vector3(renderedSize * 0.75, renderedSize * 0.08, 0)) : new THREE.Vector3(0, 0, 0); const mesh = new THREE.Mesh( new THREE.SphereGeometry(renderedSize, 24, 24), new THREE.MeshBasicMaterial({ color: star.color }), ); const halo = new THREE.Mesh( new THREE.SphereGeometry(renderedSize * 1.45, 20, 20), new THREE.MeshBasicMaterial({ color: star.color, transparent: true, opacity: starHaloOpacity(star.kind), side: THREE.BackSide, }), ); mesh.position.copy(offset); halo.position.copy(offset); root.add(mesh, halo); } return createSceneNode(root); } export function createPlanetRing(planet: PlanetSnapshot): SceneNode { const renderedPlanetRadius = celestialRenderRadius(planet.size, 0.00012, 0.03, 0.62); const ring = new THREE.Mesh( new THREE.RingGeometry(renderedPlanetRadius * 1.35, renderedPlanetRadius * 2.15, 48), new THREE.MeshBasicMaterial({ color: 0xdac89a, transparent: true, opacity: 0.42, side: THREE.DoubleSide, }), ); ring.rotation.x = Math.PI / 2; ring.rotation.z = THREE.MathUtils.degToRad(planet.orbitInclination * 0.25); return createSceneNode(ring); } function createMoonOrbit(moon: MoonSnapshot): SceneNode { const segments = 64; const period = (2 * Math.PI) / Math.max(Math.abs(moon.orbitSpeed), 1e-6); const points: THREE.Vector3[] = []; for (let i = 0; i <= segments; i++) { points.push( scaleLocalVector(computeMoonLocalPosition(moon, (i / segments) * period)) .multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE), ); } return createSceneNode(new THREE.LineLoop( new THREE.BufferGeometry().setFromPoints(points), new THREE.LineBasicMaterial({ color: moon.color, transparent: true, opacity: 0, depthWrite: false, depthTest: false, }), )); } export function createMoonVisuals(planet: PlanetSnapshot, documentRef: Document): MoonVisual[] { return planet.moons.map((moon, moonIndex) => { const moonSize = computeMoonRenderRadius(moon); const mesh = new THREE.Mesh( new THREE.SphereGeometry(moonSize, 12, 12), new THREE.MeshStandardMaterial({ color: moon.color, roughness: 0.96, metalness: 0.02, }), ); const baseColor = new THREE.Color(moon.color); const hsl = { h: 0, s: 0, l: 0 }; baseColor.getHSL(hsl); const iconColor = new THREE.Color().setHSL(hsl.h, Math.max(hsl.s, 0.4), 0.72).getStyle(); const iconBaseScale = 72; const icon = createTacticalIcon(documentRef, iconColor, iconBaseScale); return { systemId: "", planetIndex: -1, moonIndex, mesh: createSceneNode(mesh), icon, iconBaseScale, orbit: createMoonOrbit(moon), }; }); } export function createPlanetOrbit(planet: PlanetSnapshot): SceneNode { const segments = 96; const points: THREE.Vector3[] = []; for (let i = 0; i <= segments; i++) { const phase = (i / segments) * 360; points.push(scaleLocalVector(computePlanetLocalPosition(planet, 0, phase)).multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE)); } return createSceneNode(new THREE.LineLoop( new THREE.BufferGeometry().setFromPoints(points), new THREE.LineBasicMaterial({ color: planet.color, transparent: true, opacity: 0.22, depthWrite: false, depthTest: false, }), )); } export function createStationMesh(station: StationSnapshot): SceneNode { const mesh = new THREE.Mesh( new THREE.CylinderGeometry(24, 24, 18, 10), new THREE.MeshStandardMaterial({ color: station.color, emissive: new THREE.Color(station.color).multiplyScalar(0.1) }), ); mesh.rotation.x = Math.PI / 2; mesh.position.copy(toThreeVector(station.localPosition)); return createSceneNode(mesh); } export function createShipMesh(ship: ShipSnapshot, size: number, length: number, color: string): SceneNode { const geometry = new THREE.ConeGeometry(size, length, 7); geometry.rotateX(Math.PI / 2); const mesh = new THREE.Mesh( geometry, new THREE.MeshStandardMaterial({ color, emissive: new THREE.Color(color).multiplyScalar(0.18), }), ); mesh.position.copy(toThreeVector(ship.localPosition)); return createSceneNode(mesh); } function createStarGlowTexture(documentRef: Document): THREE.CanvasTexture { const canvas = documentRef.createElement("canvas"); canvas.width = 128; canvas.height = 128; const context = canvas.getContext("2d"); if (!context) { throw new Error("Unable to create star glow texture"); } const gradient = context.createRadialGradient(64, 64, 0, 64, 64, 64); gradient.addColorStop(0, "rgba(255,255,255,1)"); gradient.addColorStop(0.14, "rgba(255,255,255,0.95)"); gradient.addColorStop(0.35, "rgba(255,255,255,0.42)"); gradient.addColorStop(0.68, "rgba(180,205,255,0.1)"); gradient.addColorStop(1, "rgba(0,0,0,0)"); context.fillStyle = gradient; context.fillRect(0, 0, 128, 128); const texture = new THREE.CanvasTexture(canvas); texture.needsUpdate = true; return texture; } function createStarSparkleTexture(documentRef: Document): THREE.CanvasTexture { const canvas = documentRef.createElement("canvas"); canvas.width = 128; canvas.height = 128; const context = canvas.getContext("2d"); if (!context) { throw new Error("Unable to create star sparkle texture"); } context.clearRect(0, 0, 128, 128); context.translate(64, 64); context.lineCap = "round"; const bloom = context.createRadialGradient(0, 0, 0, 0, 0, 48); bloom.addColorStop(0, "rgba(255,255,255,0.95)"); bloom.addColorStop(0.3, "rgba(255,255,255,0.24)"); bloom.addColorStop(1, "rgba(0,0,0,0)"); context.fillStyle = bloom; context.beginPath(); context.arc(0, 0, 48, 0, Math.PI * 2); context.fill(); context.strokeStyle = "rgba(255,255,255,0.75)"; context.lineWidth = 4; context.beginPath(); context.moveTo(-38, 0); context.lineTo(38, 0); context.moveTo(0, -38); context.lineTo(0, 38); context.stroke(); context.rotate(Math.PI / 4); context.strokeStyle = "rgba(255,255,255,0.35)"; context.lineWidth = 2; context.beginPath(); context.moveTo(-28, 0); context.lineTo(28, 0); context.moveTo(0, -28); context.lineTo(0, 28); context.stroke(); const texture = new THREE.CanvasTexture(canvas); texture.needsUpdate = true; return texture; } function sampleBackdropStarColor(): THREE.Color { const roll = Math.random(); if (roll < 0.1) { return new THREE.Color().setHSL(0.08, THREE.MathUtils.randFloat(0.65, 0.9), THREE.MathUtils.randFloat(0.78, 0.9)); } if (roll < 0.28) { return new THREE.Color().setHSL(0.58, THREE.MathUtils.randFloat(0.28, 0.55), THREE.MathUtils.randFloat(0.78, 0.9)); } if (roll < 0.92) { return new THREE.Color().setHSL(0.61, THREE.MathUtils.randFloat(0.08, 0.3), THREE.MathUtils.randFloat(0.84, 0.97)); } return new THREE.Color().setHSL(0.76, THREE.MathUtils.randFloat(0.25, 0.48), THREE.MathUtils.randFloat(0.78, 0.88)); } function createStarPointLayer(radius: number, starCount: number, size: number, opacity: number): THREE.Points { const positions = new Float32Array(starCount * 3); const colors = new Float32Array(starCount * 3); for (let index = 0; index < starCount; index += 1) { const direction = new THREE.Vector3( THREE.MathUtils.randFloatSpread(2), THREE.MathUtils.randFloatSpread(2), THREE.MathUtils.randFloatSpread(2), ).normalize().multiplyScalar(radius * THREE.MathUtils.randFloat(0.83, 1)); const color = sampleBackdropStarColor().multiplyScalar(THREE.MathUtils.randFloat(0.55, 1.2)); positions[index * 3] = direction.x; positions[index * 3 + 1] = direction.y; positions[index * 3 + 2] = direction.z; colors[index * 3] = color.r; colors[index * 3 + 1] = color.g; colors[index * 3 + 2] = color.b; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3)); return new THREE.Points( geometry, new THREE.PointsMaterial({ size, sizeAttenuation: false, vertexColors: true, transparent: true, opacity, depthWrite: false, blending: THREE.AdditiveBlending, fog: false, }), ); } export function createBackdropStars(documentRef: Document): THREE.Group { const radius = 36000; const root = new THREE.Group(); root.add( createStarPointLayer(radius, 2800, 1.15, 0.5), createStarPointLayer(radius, 900, 1.9, 0.85), createStarPointLayer(radius, 240, 3.1, 0.95), ); const glowTexture = createStarGlowTexture(documentRef); const sparkleTexture = createStarSparkleTexture(documentRef); for (let index = 0; index < 72; index += 1) { const direction = new THREE.Vector3( THREE.MathUtils.randFloatSpread(2), THREE.MathUtils.randFloatSpread(2), THREE.MathUtils.randFloatSpread(2), ).normalize().multiplyScalar(radius * THREE.MathUtils.randFloat(0.84, 0.98)); const color = sampleBackdropStarColor().multiplyScalar(THREE.MathUtils.randFloat(0.9, 1.45)); const glow = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTexture, color, transparent: true, opacity: THREE.MathUtils.randFloat(0.5, 0.95), depthWrite: false, blending: THREE.AdditiveBlending, fog: false, })); const sparkle = new THREE.Sprite(new THREE.SpriteMaterial({ map: sparkleTexture, color: color.clone().lerp(new THREE.Color(0xffffff), 0.35), transparent: true, opacity: THREE.MathUtils.randFloat(0.2, 0.55), depthWrite: false, blending: THREE.AdditiveBlending, fog: false, })); const glowScale = THREE.MathUtils.randFloat(120, 260); glow.position.copy(direction); glow.scale.set(glowScale, glowScale, 1); sparkle.position.copy(direction); sparkle.material.rotation = THREE.MathUtils.randFloat(0, Math.PI); sparkle.scale.set(glowScale * THREE.MathUtils.randFloat(0.9, 1.4), glowScale * THREE.MathUtils.randFloat(0.9, 1.4), 1); root.add(glow, sparkle); } return root; } export function createPlanetTexture(color: string, seed: number, documentRef: Document): THREE.CanvasTexture { const W = 256, H = 128; const canvas = documentRef.createElement("canvas"); canvas.width = W; canvas.height = H; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Unable to create planet texture"); const imageData = ctx.createImageData(W, H); const base = new THREE.Color(color); function hash(x: number, y: number): number { const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.3) * 43758.5453; return n - Math.floor(n); } function smoothNoise(x: number, y: number): number { const ix = Math.floor(x), iy = Math.floor(y); const fx = x - ix, fy = y - iy; const ux = fx * fx * (3 - 2 * fx), uy = fy * fy * (3 - 2 * fy); const a = hash(ix, iy), b = hash(ix + 1, iy); const c = hash(ix, iy + 1), d = hash(ix + 1, iy + 1); return a + (b - a) * ux + (c - a) * uy + (a - b - c + d) * ux * uy; } function fbm(x: number, y: number): number { let v = 0, amp = 0.5, freq = 1; for (let i = 0; i < 5; i++) { v += smoothNoise(x * freq, y * freq) * amp; amp *= 0.5; freq *= 2; } return v; } for (let y = 0; y < H; y++) { for (let x = 0; x < W; x++) { const nx = (x / W) * 5, ny = (y / H) * 3; const turb = fbm(nx + 0.1, ny + 0.1) - 0.5; const band = Math.sin((y / H * 10 + turb * 3) * Math.PI); const light = 0.62 + band * 0.38; const idx = (y * W + x) * 4; imageData.data[idx] = Math.min(255, base.r * 255 * light); imageData.data[idx + 1] = Math.min(255, base.g * 255 * light); imageData.data[idx + 2] = Math.min(255, base.b * 255 * light); imageData.data[idx + 3] = 255; } } ctx.putImageData(imageData, 0, 0); const texture = new THREE.CanvasTexture(canvas); texture.wrapS = THREE.RepeatWrapping; texture.needsUpdate = true; return texture; } export function createNebulaTexture(documentRef: Document): THREE.CanvasTexture { const canvas = documentRef.createElement("canvas"); canvas.width = 512; canvas.height = 512; const context = canvas.getContext("2d"); if (!context) { throw new Error("Unable to create nebula texture"); } const palettes = [ ["rgba(80,220,255,0.24)", "rgba(120,110,255,0.18)", "rgba(255,255,255,0.14)"], ["rgba(255,130,205,0.24)", "rgba(110,170,255,0.16)", "rgba(255,240,255,0.12)"], ["rgba(120,255,205,0.2)", "rgba(100,160,255,0.18)", "rgba(255,255,255,0.1)"], ]; context.clearRect(0, 0, 512, 512); for (let layer = 0; layer < palettes.length; layer += 1) { for (let index = 0; index < 18; index += 1) { const x = THREE.MathUtils.randFloat(40, 472); const y = THREE.MathUtils.randFloat(40, 472); const radius = THREE.MathUtils.randFloat(55, 180); const [core, mid, edge] = palettes[layer]; const puff = context.createRadialGradient(x, y, 0, x, y, radius); puff.addColorStop(0, core); puff.addColorStop(0.4, mid); puff.addColorStop(0.78, edge); puff.addColorStop(1, "rgba(0,0,0,0)"); context.fillStyle = puff; context.beginPath(); context.arc(x, y, radius, 0, Math.PI * 2); context.fill(); } } for (let index = 0; index < 36; index += 1) { const x = THREE.MathUtils.randFloat(50, 462); const y = THREE.MathUtils.randFloat(50, 462); const radius = THREE.MathUtils.randFloat(18, 60); const glow = context.createRadialGradient(x, y, 0, x, y, radius); glow.addColorStop(0, "rgba(255,255,255,0.12)"); glow.addColorStop(0.4, "rgba(255,255,255,0.05)"); glow.addColorStop(1, "rgba(0,0,0,0)"); context.fillStyle = glow; context.beginPath(); context.arc(x, y, radius, 0, Math.PI * 2); context.fill(); } // Feather the entire texture toward the borders so large sprites do not show a card-like cutoff. const edgeFade = context.createRadialGradient(256, 256, 86, 256, 256, 256); edgeFade.addColorStop(0, "rgba(255,255,255,1)"); edgeFade.addColorStop(0.58, "rgba(255,255,255,0.96)"); edgeFade.addColorStop(0.82, "rgba(255,255,255,0.42)"); edgeFade.addColorStop(1, "rgba(255,255,255,0)"); context.globalCompositeOperation = "destination-in"; context.fillStyle = edgeFade; context.fillRect(0, 0, 512, 512); context.globalCompositeOperation = "source-over"; const texture = new THREE.CanvasTexture(canvas); texture.needsUpdate = true; return texture; } export function createNebulaClouds(texture: THREE.Texture): THREE.Sprite[] { const seeds = [ { direction: new THREE.Vector3(0.76, 0.28, -0.58), color: "#5bd4ff", scale: 24000, opacity: 0.22, rotation: 0.18 }, { direction: new THREE.Vector3(0.7, 0.34, -0.54), color: "#93b3ff", scale: 18000, opacity: 0.16, rotation: -0.22 }, { direction: new THREE.Vector3(-0.58, 0.24, -0.78), color: "#ff8cc6", scale: 22000, opacity: 0.2, rotation: 0.34 }, { direction: new THREE.Vector3(-0.48, 0.14, -0.86), color: "#8a8dff", scale: 16000, opacity: 0.14, rotation: -0.4 }, { direction: new THREE.Vector3(0.24, -0.46, -0.85), color: "#79ffd6", scale: 20000, opacity: 0.17, rotation: 0.52 }, { direction: new THREE.Vector3(-0.34, 0.58, 0.74), color: "#79b7ff", scale: 26000, opacity: 0.16, rotation: -0.12 }, ]; return seeds.map((seed, index) => { const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: seed.opacity, depthWrite: false, color: seed.color, blending: THREE.AdditiveBlending, fog: false, })); sprite.position.copy(seed.direction.normalize().multiplyScalar(23000 + index * 1800)); sprite.material.rotation = seed.rotation; sprite.scale.set(seed.scale, seed.scale * THREE.MathUtils.randFloat(0.52, 0.78), 1); return sprite; }); } export function createTacticalIcon(documentRef: Document, color: string, size: number): SceneNode { const canvas = documentRef.createElement("canvas"); canvas.width = 64; canvas.height = 64; const context = canvas.getContext("2d"); if (!context) { throw new Error("Unable to create tactical icon"); } context.clearRect(0, 0, 64, 64); context.strokeStyle = color; context.lineWidth = 5; context.beginPath(); context.arc(32, 32, 18, 0, Math.PI * 2); context.stroke(); const texture = new THREE.CanvasTexture(canvas); const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: texture, transparent: true, depthWrite: false, depthTest: false, color: "#ffffff", })); sprite.scale.setScalar(size); sprite.visible = false; return createSceneNode(sprite); } export function createShipTacticalIcon(documentRef: Document, color: string, size: number): SceneNode { const canvas = documentRef.createElement("canvas"); canvas.width = 128; canvas.height = 192; const context = canvas.getContext("2d"); if (!context) { throw new Error("Unable to create ship tactical icon"); } context.clearRect(0, 0, canvas.width, canvas.height); context.lineCap = "round"; context.lineJoin = "round"; context.strokeStyle = color; context.fillStyle = "rgba(7, 16, 30, 0.8)"; context.lineWidth = 6; context.globalAlpha = 0.92; context.beginPath(); context.moveTo(64, 182); context.lineTo(64, 108); context.stroke(); context.globalAlpha = 1; context.beginPath(); context.arc(64, 70, 26, 0, Math.PI * 2); context.fill(); context.stroke(); context.beginPath(); context.moveTo(64, 34); context.lineTo(64, 49); context.stroke(); context.beginPath(); context.moveTo(64, 91); context.lineTo(64, 106); context.stroke(); context.beginPath(); context.moveTo(28, 70); context.lineTo(43, 70); context.stroke(); context.beginPath(); context.moveTo(85, 70); context.lineTo(100, 70); context.stroke(); context.beginPath(); context.moveTo(44, 116); context.lineTo(64, 136); context.lineTo(84, 116); context.stroke(); const texture = new THREE.CanvasTexture(canvas); const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: texture, transparent: true, depthWrite: false, depthTest: false, color: "#ffffff", fog: false, })); sprite.center.set(0.5, 0.08); sprite.scale.set(size * 1.2, size * 1.8, 1); sprite.visible = false; return createSceneNode(sprite); } export function createStarDot(documentRef: Document, color: string): SceneNode { const canvas = documentRef.createElement("canvas"); canvas.width = 32; canvas.height = 32; const context = canvas.getContext("2d"); if (!context) { throw new Error("Unable to create star dot canvas"); } context.clearRect(0, 0, 32, 32); context.fillStyle = color; context.beginPath(); context.arc(16, 16, 12, 0, Math.PI * 2); context.fill(); const texture = new THREE.CanvasTexture(canvas); const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: texture, transparent: true, depthWrite: false, depthTest: false, color: "#ffffff", fog: false, })); sprite.scale.setScalar(4); sprite.visible = false; return createSceneNode(sprite); } export function createShellReticle(documentRef: Document, color: string, size: number): SceneNode { const canvas = documentRef.createElement("canvas"); canvas.width = 128; canvas.height = 128; const context = canvas.getContext("2d"); if (!context) { throw new Error("Unable to create shell reticle"); } context.clearRect(0, 0, 128, 128); context.strokeStyle = color; context.lineWidth = 6; context.globalAlpha = 0.58; context.beginPath(); context.arc(64, 64, 48, 0.12 * Math.PI, 0.34 * Math.PI); context.stroke(); context.beginPath(); context.arc(64, 64, 48, 0.62 * Math.PI, 0.84 * Math.PI); context.stroke(); context.beginPath(); context.arc(64, 64, 48, 1.12 * Math.PI, 1.34 * Math.PI); context.stroke(); context.beginPath(); context.arc(64, 64, 48, 1.62 * Math.PI, 1.84 * Math.PI); context.stroke(); const texture = new THREE.CanvasTexture(canvas); const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthWrite: false, depthTest: false, color, opacity: 1, blending: THREE.AdditiveBlending, fog: false, }); const sprite = createSceneNode(new THREE.Sprite(material)); sprite.setScaleScalar(size); sprite.setVisible(false); sprite.setRenderOrder(1000); return sprite; }