A ring of text without using torus?

Question:
I’m trying to create a ring of text that rotates around a box in the middle like how gyroscope ring rotates around the middle, or that gyroscopic bbq grill. It’s sort of like the picture provided, but with thin text that rotates along its x y and z.

I want it to be pure text and not just text wrapped around a torus, because then the text isn’t readable, and is oddly streched.

What I tried:
Using a torus:
i made a canvas that renders the text (it’s a foreign language), creates a texture from the canvas, apply that texture onto torus, and then try and adjust the wrapping to make it look clean.

code:


// Create the canvas and render the Urdu text
const canvas = document.createElement('canvas');
canvas.width = 1024;
canvas.height = 512;
const context = canvas.getContext('2d');
context.fillStyle = '#000000'; // Black background
context.fillRect(0, 0, canvas.width, canvas.height);

// Set text properties
context.fillStyle = '#ffffff'; // White 
context.font = '48px "Noto Nastaliq Urdu", "Arial", sans-serif';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.direction = 'rtl';

const urduText = 'کبھی کبھی مرے دل میں خیال آتا ہے کہ زندگی تری زلفوں کی نرم چھاؤں میں گزرنے پاتی تو شاداب ہو بھی سکتی تھی';

// Function to wrap the Urdu text
function wrapText(context, text, maxWidth) {
  const words = text.split(' ').reverse();
  let lines = [];
  let line = '';

  for (let n = 0; n < words.length; n++) {
    const testLine = words[n] + ' ' + line;
    const metrics = context.measureText(testLine);
    const testWidth = metrics.width;

    if (testWidth > maxWidth && n > 0) {
      lines.push(line.trim());
      line = words[n] + ' ';
    } else {
      line = testLine;
    }
  }
  lines.push(line.trim());
  return lines;
}

const lines = wrapText(context, urduText, canvas.width - 50);
const lineHeight = 60;
let y = canvas.height / 2 - ((lines.length - 1) / 2) * lineHeight;

for (const line of lines) {
  context.fillText(line, canvas.width / 2, y);
  y += lineHeight;
}

const torusTexture = new THREE.CanvasTexture(canvas);

// Adjust texture wrapping 
torusTexture.wrapS = THREE.RepeatWrapping;
torusTexture.wrapT = THREE.ClampToEdgeWrapping;
torusTexture.repeat.set(1, 1);

// the torus  
const torusMaterial = new THREE.MeshBasicMaterial({ map: torusTexture });
const geometry = new THREE.TorusGeometry(10, 0.5, 16, 100);
const torus = new THREE.Mesh(geometry, torusMaterial);
torus.rotation.x = Math.PI / 2;

scene.add(torus);

but this gives me a very ugly torus with stretched text inside

how can I possibly not use torus at all and instead create a ring of text that acts like torus.

thanks

Familiar example, I think I’ve seen that at Codepen but the closest I can find is this.

That’s a very long line of text, which is going to take a considerable canvas size to display with decent resolution. It looks like you’re trying to wrap the text, but I think the issue is transforming the canvas to fit contents. Try viewing that 2D canvas.

Maybe you don’t want a torus, though? You could use a cylinder, or some extruded mesh.

text-1

// Scene, Camera, Renderer setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 15;
const renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0xffffff);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Create the Canvas and Context for Text
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = 2048;
canvas.height = 256;

function drawText(text) {
  // Clear the canvas
  context.fillStyle = '#000';
  context.fillRect(0, 0, canvas.width, canvas.height);

  // Set font and measure text width
  context.font = '128px Arial';
  const textWidth = context.measureText(text).width;

  // Scale the text to fit within the canvas width
  if (textWidth > canvas.width) { // Adjust padding as needed
    const scale = (canvas.width) / textWidth;
    context.setTransform(scale, 0, 0, 1, 50, canvas.height / 2); // Scale horizontally to fit
  } else {
    context.setTransform(1, 0, 0, 1, 50, canvas.height / 2); // No scaling needed
  }

  // Draw the text
  context.fillStyle = 'gold';
  context.fillText(text, 0, 48);

  // Reset transformation matrix
  context.setTransform(1, 0, 0, 1, 0, 0);
}

// Create Texture from Canvas
const texture = new THREE.CanvasTexture(canvas);

// Create a Thin Cylinder (Ribbon) with the Texture
const geometry = new THREE.CylinderGeometry(5, 5, 3, 64, 1, true);
const material = new THREE.MeshBasicMaterial({ map: texture, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// Draw the text and update the texture
drawText("کبھی کبھی مرے دل میں خیال آتا ہے کہ زندگی تری زلفوں کی نرم چھاؤں میں گزرنے پاتی تو شاداب ہو بھی سکتی تھی");
texture.needsUpdate = true;

// Animation Loop
renderer.setAnimationLoop(() => {
  renderer.render(scene, camera);
})

// Add OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);

// Handle window resize
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

If the geometry is not proportional to the canvas, you’ll start to encounter stretching.

Otherwise, you could do a marquee effect to scroll text:

text-2

let text = "کبھی کبھی مرے دل میں خیال آتا ہے کہ زندگی تری زلفوں کی نرم چھاؤں میں گزرنے پاتی تو شاداب ہو بھی سکتی تھی";
let textPosition = canvas.width;

function drawText() {
  // Clear the canvas
  context.fillStyle = '#000';
  context.fillRect(0, 0, canvas.width, canvas.height);

  // Set font and measure text width
  context.font = '128px Arial';
  context.fillStyle = 'gold';
  context.fillText(text, textPosition, 160);
}

// ...

// Animation Loop
renderer.setAnimationLoop(() => {
  // Update the text position for scrolling effect
  textPosition -= 2;
  if (textPosition < -context.measureText(text).width) {
    textPosition = canvas.width;
  }

  // Redraw the text and update the texture
  drawText();
  texture.needsUpdate = true;

  renderer.render(scene, camera);
})

That would eliminate the need to fit text onto a surface.

An entirely different approach might be ideal, depending on what you’re trying to do.

2 Likes

see from the Collection of examples from discourse.threejs.org

TransparentCylinderWithText: eXtended eXamples 2020

Ur code gave me inspo, and i’ve found another way using troika-three-text
similar to yours, i ditched the torus thing and tried this. Basically, i split each word and for each char, i’ll use trig to find the position it should be in on the circle, then it’s rotated, and then added to the group and the scene.

const textGroup = new THREE.Group();
const urduText = 'کبھی کبھی مرے دل میں خیال آتا ہے کہ زندگی تری زلفوں کی نرم چھاؤں میں گزرنے پاتی تو شاداب ہو بھی سکتی تھی';
const chars = urduText.split('');
const angleStep = (Math.PI * 2) / chars.length;
const radius = 30;

chars.forEach((char, i) => {
  const textMesh = new Text();
  textMesh.text = char;
  textMesh.font = '/font/GandharaSulsRegular.ttf';
  textMesh.fontSize = 2;
  textMesh.color = 0xffffff;
  textMesh.anchorX = 'center';
  textMesh.anchorY = 'middle';
  // textMesh.material = textMaterial;

  const angle = i * angleStep;

  textMesh.position.x = radius * Math.cos(angle);
  textMesh.position.z = radius * Math.sin(angle);
  textMesh.position.y += 0

  textMesh.rotation.y = -angle + Math.PI / 2;

  textGroup.add(textMesh);

  textMesh.sync();
});

// textGroup.position.copy(jef.position);
scene.add(textGroup);

and then in the animate function, rotation:

textGroup.rotation.x += 0.0001;
    textGroup.rotation.y += 0.0001;
    textGroup.rotation.z += 0.003;
1 Like