three-slug 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/demo/main.js ADDED
@@ -0,0 +1,357 @@
1
+
2
+ import * as THREE from 'three';
3
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
4
+
5
+ import { SlugMaterial, SlugGeometry, SlugGenerator, SlugLoader, injectSlug } from '../src/index.js';
6
+
7
+ // Only handles some generic unicode:
8
+ // 漢字 ॐ ♞ ♠ ♡ ♢ ♣ ☻ ☼ 🎵 🚀 (Demonstrating generic Unicode vectors)
9
+ // ✌️🌴🐢🐐🍄⚽🍻👑📸😬👀🚨🏡🕊️🏆😻🌟🧿🍀🎨🍜
10
+
11
+
12
+ let camera, scene, renderer;
13
+ let controls;
14
+ let slugMesh;
15
+ let spotLight;
16
+ let pointLight;
17
+ let debugCube;
18
+ let loadedData = null;
19
+ let loadedFileName = 'font';
20
+
21
+ init();
22
+ animate();
23
+
24
+ function init() {
25
+ renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
26
+ renderer.setPixelRatio(window.devicePixelRatio);
27
+ renderer.setSize(window.innerWidth, window.innerHeight);
28
+ renderer.setClearColor(0x000000, 1.0); // Pure black background
29
+ renderer.shadowMap.enabled = true;
30
+ renderer.shadowMap.type = THREE.PCFShadowMap;
31
+ renderer.shadowMap.samples = 4;
32
+ document.body.appendChild(renderer.domElement);
33
+
34
+ // Swap to Perspective camera to fly around
35
+ camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);
36
+ camera.position.set(307, -500, 400);
37
+
38
+ scene = new THREE.Scene();
39
+
40
+ const ambientLight = new THREE.AmbientLight(0x404040, 0.2); // Very low ambient to keep shadows dark
41
+ scene.add(ambientLight);
42
+
43
+ // Extreme intensity for high contrast, positioned head-on to cast perfectly square shadows onto the backend plane
44
+ spotLight = new THREE.SpotLight(0xffffff, 5000000.0);
45
+ spotLight.position.set(0, 0, 800);
46
+ spotLight.angle = Math.PI / 4;
47
+ spotLight.penumbra = 0.5;
48
+ spotLight.decay = 2.0;
49
+ spotLight.distance = 3000;
50
+ spotLight.castShadow = true;
51
+ spotLight.shadow.bias = -0.0001; // Tiny absolute depth offset to kill shadow acne
52
+ spotLight.shadow.normalBias = 0.05; // Slightly push the intersection along the normal
53
+ spotLight.shadow.mapSize.width = 2048;
54
+ spotLight.shadow.mapSize.height = 2048;
55
+ spotLight.shadow.camera.near = 10;
56
+ spotLight.shadow.camera.far = 2000;
57
+ scene.add(spotLight);
58
+
59
+ // Explicitly add target so Three.js auto-updates its matrices
60
+ spotLight.target.position.set(0, 0, -100);
61
+ scene.add(spotLight.target);
62
+
63
+ const planeGeometry = new THREE.PlaneGeometry(10000, 10000);
64
+ const planeMaterial = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.8 });
65
+ const plane = new THREE.Mesh(planeGeometry, planeMaterial);
66
+ plane.position.z = -10; // Moved explicitly further back so depth bounds are 100% unambiguous in shadow map
67
+ plane.receiveShadow = true;
68
+ scene.add(plane);
69
+
70
+ // Floating Debug Cube to cast shadows on text
71
+ const cubeGeo = new THREE.BoxGeometry(20, 20, 20);
72
+ const cubeMat = new THREE.MeshStandardMaterial({ color: 0xff3333, roughness: 0.1 });
73
+ debugCube = new THREE.Mesh(cubeGeo, cubeMat);
74
+ debugCube.position.set(0, 0, 50);
75
+ debugCube.castShadow = true;
76
+ scene.add(debugCube);
77
+
78
+ // Small blue point light that orbits the debug cube
79
+ pointLight = new THREE.PointLight(0x00aaff, 200000.0, 2500); // Increased intensity + reach radius
80
+ const bulbGeo = new THREE.SphereGeometry(10, 16, 8); // Larger visible sphere
81
+ const bulbMat = new THREE.MeshBasicMaterial({ color: 0x00aaff });
82
+ pointLight.add(new THREE.Mesh(bulbGeo, bulbMat));
83
+ pointLight.castShadow = true;
84
+ pointLight.shadow.bias = -0.0001;
85
+ scene.add(pointLight);
86
+
87
+ controls = new OrbitControls(camera, renderer.domElement);
88
+ controls.enableDamping = true;
89
+ controls.target.set(207, 0, 0);
90
+ window.addEventListener('resize', onWindowResize);
91
+
92
+ document.getElementById('fileSluggish').addEventListener('change', handleSluggishUpload);
93
+ document.getElementById('fileTtf').addEventListener('change', handleTtfUpload);
94
+ document.getElementById('btnDownload').addEventListener('click', handleDownload);
95
+ document.getElementById('textInput').addEventListener('input', () => {
96
+ if (loadedData) createTextMesh();
97
+ });
98
+
99
+ document.getElementById('fontSelect').addEventListener('change', (e) => {
100
+ loadFont(e.target.value);
101
+ });
102
+
103
+ document.getElementById('textInput').addEventListener('input', () => {
104
+ if (loadedData) createTextMesh();
105
+ });
106
+
107
+ document.getElementById('useRawShader').addEventListener('change', () => {
108
+ if (loadedData) createTextMesh();
109
+ });
110
+
111
+ document.getElementById('textJustify').addEventListener('change', () => {
112
+ if (loadedData) createTextMesh();
113
+ });
114
+
115
+ // Prepopulate textarea with our own source code to demonstrate large paragraphs
116
+ fetch('./main.js')
117
+ .then(response => response.text())
118
+ .then(text => {
119
+ const textArea = document.getElementById('textInput');
120
+ textArea.value = text; // 1x copies
121
+ if (loadedData) createTextMesh();
122
+ })
123
+ .catch(err => console.error("Could not load main.js for textarea", err));
124
+
125
+ // Dynamically populate font select from index
126
+ const select = document.getElementById('fontSelect');
127
+ fetch('./fonts/fonts.json')
128
+ .then(r => r.json())
129
+ .then(fonts => {
130
+ fonts.forEach(f => {
131
+ const opt = document.createElement('option');
132
+ opt.value = f;
133
+ opt.innerText = f.replace(/\.[^/.]+$/, "");
134
+ select.appendChild(opt);
135
+ });
136
+ if (select.children.length > 0) {
137
+ loadFont(select.value);
138
+ } else {
139
+ loadFont('DejaVuSansMono.ttf');
140
+ }
141
+ })
142
+ .catch(err => {
143
+ console.error("Could not load fonts.json list:", err);
144
+ loadFont('DejaVuSansMono.ttf'); // Fallback
145
+ });
146
+ }
147
+
148
+ async function loadFont(fontName) {
149
+ try {
150
+ console.log(`Loading font: ${fontName}...`);
151
+ const response = await fetch(`./fonts/${fontName}`);
152
+ const buffer = await response.arrayBuffer();
153
+ const generator = new SlugGenerator();
154
+ loadedData = await generator.generateFromBuffer(buffer);
155
+ loadedFileName = fontName.replace(/\.[^/.]+$/, "");
156
+ console.log(`Loaded font data for ${fontName}:`, loadedData);
157
+ createTextMesh();
158
+ } catch (err) {
159
+ console.error(`Failed to load font ${fontName}`, err);
160
+ }
161
+ }
162
+
163
+ function handleSluggishUpload(event) {
164
+ const file = event.target.files[0];
165
+ if (!file) return;
166
+
167
+ const reader = new FileReader();
168
+ reader.onload = function (e) {
169
+ const buffer = e.target.result;
170
+ try {
171
+ const loader = new SlugLoader();
172
+ loadedData = loader.parse(buffer);
173
+ console.log("Loaded Sluggish Data:", loadedData);
174
+ createTextMesh();
175
+ } catch (err) {
176
+ console.error(err);
177
+ alert("Error parsing sluggish file: " + err.message);
178
+ }
179
+ };
180
+ reader.readAsArrayBuffer(file);
181
+ }
182
+
183
+ async function handleTtfUpload(event) {
184
+ const file = event.target.files[0];
185
+ if (!file) return;
186
+
187
+ loadedFileName = file.name.replace(/\.[^/.]+$/, "");
188
+
189
+ try {
190
+ console.log("Generating slug data from TTF...");
191
+ const generator = new SlugGenerator();
192
+ loadedData = await generator.generateFromFile(file);
193
+ console.log("Generated Data:", loadedData);
194
+ createTextMesh();
195
+ } catch (err) {
196
+ console.error(err);
197
+ alert("Error generating sluggish data: " + err.message);
198
+ }
199
+ }
200
+
201
+ function handleDownload() {
202
+ if (!loadedData || !loadedData._raw) {
203
+ alert("No generated TTF data to download. Please load a .ttf file first.");
204
+ return;
205
+ }
206
+ const generator = new SlugGenerator();
207
+ const buffer = generator.exportSluggish(loadedData);
208
+
209
+ const blob = new Blob([buffer], { type: "application/octet-stream" });
210
+ const url = URL.createObjectURL(blob);
211
+ const a = document.createElement('a');
212
+ a.href = url;
213
+ a.download = loadedFileName + ".sluggish";
214
+ document.body.appendChild(a);
215
+ a.click();
216
+ document.body.removeChild(a);
217
+ URL.revokeObjectURL(url);
218
+ }
219
+
220
+ function createTextMesh() {
221
+ if (!loadedData) return;
222
+
223
+ if (slugMesh) {
224
+ scene.remove(slugMesh);
225
+ slugMesh.geometry.dispose();
226
+ slugMesh.material.dispose();
227
+ }
228
+
229
+ const useRawFallback = document.getElementById('useRawShader').checked;
230
+ let material;
231
+
232
+
233
+
234
+ const textToRender = document.getElementById('textInput').value;
235
+ const justifyMode = document.getElementById('textJustify').value;
236
+
237
+ // Dynamically size the geometry capacity to exactly the number of characters
238
+ // Add a minimum of 1 to avoid Three.js throwing empty buffer errors
239
+ const geometry = new SlugGeometry(Math.max(1, textToRender.length));
240
+
241
+ geometry.clear();
242
+
243
+ geometry.addText(textToRender, loadedData, {
244
+ fontScale: 0.5,
245
+ startX: 0,
246
+ startY: 0,
247
+ justify: justifyMode
248
+ });
249
+
250
+ if (useRawFallback) {
251
+ material = new SlugMaterial({
252
+ curvesTex: loadedData.curvesTex,
253
+ bandsTex: loadedData.bandsTex
254
+ });
255
+ // No custom shadow material for raw shader fallback
256
+ } else {
257
+ material = new THREE.MeshStandardMaterial({
258
+ color: 0xffaa00, // Golden yellow
259
+ roughness: 1.0, // Pure diffuse plastic surface to properly scatter un-angled SpotLight luminance
260
+ metalness: 0.0, // Removing metalness prevents the flat quads from reflecting the void into a dark mirror
261
+ side: THREE.DoubleSide
262
+ });
263
+ }
264
+
265
+ let newSlugMesh = new THREE.Mesh(geometry, material);
266
+ if (slugMesh) {
267
+ newSlugMesh.position.copy(slugMesh.position);
268
+ }
269
+ slugMesh = newSlugMesh;
270
+
271
+ // Apply the architectural PBR macros and instantiate Shared Global Shadow maps securely
272
+ injectSlug(slugMesh, material, loadedData);
273
+
274
+ // Let Three.js dynamically build the Depth Material derived from our onBeforeCompile graph instead of forcing custom
275
+ slugMesh.castShadow = true;
276
+ slugMesh.receiveShadow = true;
277
+
278
+ // Disabling frustum culling temporally to purely isolate WebGL shadow buffer pipelines
279
+ //slugMesh.frustumCulled = false;
280
+ slugMesh.scale.multiplyScalar(.02);
281
+ scene.add(slugMesh);
282
+ }
283
+
284
+ function onWindowResize() {
285
+ camera.aspect = window.innerWidth / window.innerHeight;
286
+ camera.updateProjectionMatrix();
287
+ renderer.setSize(window.innerWidth, window.innerHeight);
288
+ }
289
+
290
+ function animate() {
291
+ requestAnimationFrame(animate);
292
+
293
+ // Star Wars Title Crawl
294
+ if (slugMesh) {
295
+ const autoScroll = document.getElementById('autoScroll');
296
+ if (autoScroll && autoScroll.checked) {
297
+ const speed = 1.7;
298
+ slugMesh.position.y += speed;
299
+ if (slugMesh.position.y > 1500) {
300
+ slugMesh.position.y -= 1000;
301
+ }
302
+ }
303
+
304
+ const glitchCheckbox = document.getElementById('matrixGlitch');
305
+ if (glitchCheckbox && glitchCheckbox.checked && loadedData) {
306
+ if (Math.random() < 0.25) { // 25% chance per frame to glitch
307
+ let currentText = document.getElementById('textInput').value.split('');
308
+ let numGlitches = Math.floor(Math.random() * 1800) + 1;
309
+ const keys = Array.from(loadedData.codePoints.keys()).filter(k => k > 32); // Exclude space and hidden ones
310
+
311
+ for (let i = 0; i < numGlitches; i++) {
312
+ let idx = Math.floor(Math.random() * currentText.length);
313
+ if (currentText[idx] !== '\n' && currentText[idx] !== ' ') {
314
+ let randomChar = String.fromCodePoint(keys[Math.floor(Math.random() * keys.length)]);
315
+ currentText[idx] = randomChar;
316
+ }
317
+ }
318
+
319
+ slugMesh.geometry.clear();
320
+ slugMesh.geometry.addText(currentText.join(''), loadedData, {
321
+ fontScale: 0.5,
322
+ startX: 0,
323
+ startY: 0,
324
+ justify: document.getElementById('textJustify').value
325
+ });
326
+ slugMesh.userData.isGlitched = true;
327
+ }
328
+ } else if (slugMesh.userData.isGlitched) {
329
+ // Restore original clean text immediately when unchecked
330
+ slugMesh.geometry.clear();
331
+ slugMesh.geometry.addText(document.getElementById('textInput').value, loadedData, {
332
+ fontScale: 0.5,
333
+ startX: 0,
334
+ startY: 0,
335
+ justify: document.getElementById('textJustify').value
336
+ });
337
+ slugMesh.userData.isGlitched = false;
338
+ }
339
+ }
340
+
341
+ if (debugCube) {
342
+ // Keep debug cube visible in frame and spinning wildly
343
+ debugCube.position.y = 50;
344
+ debugCube.position.x = Math.sin(Date.now() * 0.001) * 300;
345
+ debugCube.rotation.x += 0.01;
346
+ debugCube.rotation.y += 0.02;
347
+
348
+ if (pointLight) {
349
+ pointLight.position.x = debugCube.position.x + Math.cos(Date.now() * 0.0015) * 300;
350
+ pointLight.position.y = debugCube.position.y + Math.sin(Date.now() * 0.0015) * 300;
351
+ pointLight.position.z = debugCube.position.z + Math.cos(Date.now() * 0.001) * 150;
352
+ }
353
+ }
354
+
355
+ if (controls) controls.update();
356
+ renderer.render(scene, camera);
357
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "dependencies": {
3
+ "opentype.js": "^1.3.4"
4
+ },
5
+ "name": "three-slug",
6
+ "version": "1.0.0",
7
+ "description": "JSlug is a Javascript and WebGL port of Eric Lengyel's **Slug** font rendering algorithm, implemented for **Three.js**.",
8
+ "main": "src/index.js",
9
+ "module": "src/index.js",
10
+ "exports": {
11
+ ".": "./src/index.js"
12
+ },
13
+ "type": "module",
14
+ "peerDependencies": {
15
+ "three": ">=0.160.0"
16
+ },
17
+ "devDependencies": {},
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/manthrax/JSlug.git"
21
+ },
22
+ "keywords": [],
23
+ "author": "manthrax",
24
+ "license": "ISC",
25
+ "bugs": {
26
+ "url": "https://github.com/manthrax/JSlug/issues"
27
+ },
28
+ "homepage": "https://github.com/manthrax/JSlug#readme"
29
+ }
package/screenshot.png ADDED
Binary file