three-slug 1.0.0 → 1.1.1

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/README.md CHANGED
@@ -6,6 +6,10 @@
6
6
 
7
7
  Unlike traditional MSDF (Multi-Channel Signed Distance Field) font rendering which can suffer from corner rounding and texture resolution limits, the Slug algorithm evaluates the quadratic bezier curves of the TrueType font directly within the fragment shader. This enables resolution-independent font rendering, sharp corners, and precise anti-aliasing.
8
8
 
9
+ ## Demo
10
+
11
+ [Demo](https://manthrax.github.io/JSlug/demo/)
12
+
9
13
  ## Screenshots
10
14
 
11
15
  ![three-slug Rendering Demo](screenshot.png)
Binary file
@@ -8,5 +8,6 @@
8
8
  "SpaceMono-Regular.ttf",
9
9
  "SpaceMono-Bold.ttf",
10
10
  "SpaceMono-Italic.ttf",
11
- "SpaceMono-BoldItalic.ttf"
12
- ]
11
+ "SpaceMono-BoldItalic.ttf",
12
+ "Barbarian.ttf"
13
+ ]
package/demo/index.html CHANGED
@@ -56,8 +56,9 @@
56
56
  </div>
57
57
  <hr style="border-color: #444; margin: 15px 0;">
58
58
  <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
59
- <label><input type="checkbox" id="autoScroll" checked /> Auto Scroll</label>
59
+ <label><input type="checkbox" id="autoScroll" /> Auto Scroll</label>
60
60
  <label><input type="checkbox" id="matrixGlitch" /> Matrix Glitch</label>
61
+ <label><input type="checkbox" id="testMap" /> Test Map</label>
61
62
  <label><input type="checkbox" id="useRawShader" /> RawShader Fallback</label>
62
63
  </div>
63
64
  <div style="margin-bottom: 15px;">
package/demo/main.js CHANGED
@@ -1,357 +1,391 @@
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
- }
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 = 1024;
54
+ spotLight.shadow.mapSize.height = 1024;
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('testMap').addEventListener('change', () => {
112
+ if (loadedData) createTextMesh();
113
+ });
114
+
115
+ document.getElementById('textJustify').addEventListener('change', () => {
116
+ if (loadedData) createTextMesh();
117
+ });
118
+
119
+ // Prepopulate textarea with our own source code to demonstrate large paragraphs
120
+ fetch('./main.js')
121
+ .then(response => response.text())
122
+ .then(text => {
123
+ const textArea = document.getElementById('textInput');
124
+ textArea.value = text; // 1x copies
125
+ if (loadedData) createTextMesh();
126
+ })
127
+ .catch(err => console.error("Could not load main.js for textarea", err));
128
+
129
+ // Dynamically populate font select from index
130
+ const select = document.getElementById('fontSelect');
131
+ fetch('./fonts/fonts.json')
132
+ .then(r => r.json())
133
+ .then(fonts => {
134
+ fonts.forEach(f => {
135
+ const opt = document.createElement('option');
136
+ opt.value = f;
137
+ opt.innerText = f.replace(/\.[^/.]+$/, "");
138
+ select.appendChild(opt);
139
+ });
140
+ if (select.children.length > 0) {
141
+ loadFont(select.value);
142
+ } else {
143
+ loadFont('DejaVuSansMono.ttf');
144
+ }
145
+ })
146
+ .catch(err => {
147
+ console.error("Could not load fonts.json list:", err);
148
+ loadFont('DejaVuSansMono.ttf'); // Fallback
149
+ });
150
+ }
151
+
152
+ async function loadFont(fontName) {
153
+ try {
154
+ console.log(`Loading font: ${fontName}...`);
155
+ const response = await fetch(`./fonts/${fontName}`);
156
+ const buffer = await response.arrayBuffer();
157
+ const generator = new SlugGenerator();
158
+ loadedData = await generator.generateFromBuffer(buffer);
159
+ loadedFileName = fontName.replace(/\.[^/.]+$/, "");
160
+ console.log(`Loaded font data for ${fontName}:`, loadedData);
161
+ createTextMesh();
162
+ } catch (err) {
163
+ console.error(`Failed to load font ${fontName}`, err);
164
+ }
165
+ }
166
+
167
+ function handleSluggishUpload(event) {
168
+ const file = event.target.files[0];
169
+ if (!file) return;
170
+
171
+ const reader = new FileReader();
172
+ reader.onload = function (e) {
173
+ const buffer = e.target.result;
174
+ try {
175
+ const loader = new SlugLoader();
176
+ loadedData = loader.parse(buffer);
177
+ console.log("Loaded Sluggish Data:", loadedData);
178
+ createTextMesh();
179
+ } catch (err) {
180
+ console.error(err);
181
+ alert("Error parsing sluggish file: " + err.message);
182
+ }
183
+ };
184
+ reader.readAsArrayBuffer(file);
185
+ }
186
+
187
+ async function handleTtfUpload(event) {
188
+ const file = event.target.files[0];
189
+ if (!file) return;
190
+
191
+ loadedFileName = file.name.replace(/\.[^/.]+$/, "");
192
+
193
+ try {
194
+ console.log("Generating slug data from TTF...");
195
+ const generator = new SlugGenerator();
196
+ loadedData = await generator.generateFromFile(file);
197
+ console.log("Generated Data:", loadedData);
198
+ createTextMesh();
199
+ } catch (err) {
200
+ console.error(err);
201
+ alert("Error generating sluggish data: " + err.message);
202
+ }
203
+ }
204
+
205
+ function handleDownload() {
206
+ if (!loadedData || !loadedData._raw) {
207
+ alert("No generated TTF data to download. Please load a .ttf file first.");
208
+ return;
209
+ }
210
+ const generator = new SlugGenerator();
211
+ const buffer = generator.exportSluggish(loadedData);
212
+
213
+ const blob = new Blob([buffer], { type: "application/octet-stream" });
214
+ const url = URL.createObjectURL(blob);
215
+ const a = document.createElement('a');
216
+ a.href = url;
217
+ a.download = loadedFileName + ".sluggish";
218
+ document.body.appendChild(a);
219
+ a.click();
220
+ document.body.removeChild(a);
221
+ URL.revokeObjectURL(url);
222
+ }
223
+
224
+ function createTextMesh() {
225
+ if (!loadedData) return;
226
+
227
+ if (slugMesh) {
228
+ scene.remove(slugMesh);
229
+ slugMesh.geometry.dispose();
230
+ slugMesh.material.dispose();
231
+ }
232
+
233
+ const useRawFallback = document.getElementById('useRawShader').checked;
234
+ const testMapChecked = document.getElementById('testMap').checked;
235
+ let material;
236
+
237
+ // Create a procedural grid texture to test local UV-mapping layout support
238
+ const canvas = document.createElement('canvas');
239
+ canvas.width = 128;
240
+ canvas.height = 128;
241
+ const ctx = canvas.getContext('2d');
242
+
243
+ // Draw highly contrasting 2x2 Checkerboard
244
+ ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, 128, 128);
245
+ ctx.fillStyle = '#ee0088'; // Vibrant magenta for clear grid inspection
246
+ ctx.fillRect(0, 0, 64, 64);
247
+ ctx.fillRect(64, 64, 64, 64);
248
+
249
+ const gridTex = new THREE.CanvasTexture(canvas);
250
+ gridTex.wrapS = THREE.RepeatWrapping;
251
+ gridTex.wrapT = THREE.RepeatWrapping;
252
+ gridTex.repeat.set(.5, .5);
253
+ gridTex.magFilter = THREE.NearestFilter;
254
+ gridTex.minFilter = THREE.NearestFilter;
255
+
256
+
257
+
258
+ const textToRender = document.getElementById('textInput').value;
259
+ const justifyMode = document.getElementById('textJustify').value;
260
+
261
+ // Dynamically size the geometry capacity to exactly the number of characters
262
+ // Add a minimum of 1 to avoid Three.js throwing empty buffer errors
263
+ const geometry = new SlugGeometry(Math.max(1, textToRender.length));
264
+
265
+ geometry.clear();
266
+
267
+ geometry.addText(textToRender, loadedData, {
268
+ fontScale: 0.5,
269
+ startX: 0,
270
+ startY: 0,
271
+ justify: justifyMode
272
+ });
273
+
274
+ if (useRawFallback) {
275
+ material = new SlugMaterial({
276
+ curvesTex: loadedData.curvesTex,
277
+ bandsTex: loadedData.bandsTex
278
+ });
279
+ // No custom shadow material for raw shader fallback
280
+ } else {
281
+ material = new THREE.MeshStandardMaterial({
282
+ color: 0xffffff,//0x00aaff, // Golden yellow
283
+ roughness: 0.1, // Pure diffuse plastic surface to properly scatter un-angled SpotLight luminance
284
+ metalness: 0.05, // Removing metalness prevents the flat quads from reflecting the void into a dark mirror
285
+ side: THREE.DoubleSide,
286
+ map: testMapChecked ? gridTex : null
287
+ });
288
+
289
+ if (testMapChecked) {
290
+ material.defines = { SLUG_MODELSPACE_UV: '' };
291
+ material.needsUpdate = true;
292
+ }
293
+ }
294
+
295
+ let newSlugMesh = new THREE.Mesh(geometry, material);
296
+ if (slugMesh) {
297
+ newSlugMesh.position.copy(slugMesh.position);
298
+ }
299
+ slugMesh = newSlugMesh;
300
+
301
+ // Apply the architectural PBR macros and instantiate Shared Global Shadow maps securely
302
+ injectSlug(slugMesh, material, loadedData);
303
+
304
+ // Let Three.js dynamically build the Depth Material derived from our onBeforeCompile graph instead of forcing custom
305
+ slugMesh.castShadow = true;
306
+ slugMesh.receiveShadow = true;
307
+
308
+ // Disabling frustum culling temporally to purely isolate WebGL shadow buffer pipelines
309
+ //slugMesh.frustumCulled = false;
310
+ slugMesh.scale.multiplyScalar(.02);
311
+ scene.add(slugMesh);
312
+ }
313
+
314
+ function onWindowResize() {
315
+ camera.aspect = window.innerWidth / window.innerHeight;
316
+ camera.updateProjectionMatrix();
317
+ renderer.setSize(window.innerWidth, window.innerHeight);
318
+ }
319
+
320
+ function animate() {
321
+ requestAnimationFrame(animate);
322
+
323
+ // Star Wars Title Crawl
324
+ if (slugMesh) {
325
+ const autoScroll = document.getElementById('autoScroll');
326
+ if (autoScroll && autoScroll.checked) {
327
+ const speed = 1.7;
328
+ slugMesh.position.y += speed;
329
+ if (slugMesh.position.y > 1500) {
330
+ slugMesh.position.y -= 1000;
331
+ }
332
+ }
333
+ if (slugMesh.material.map) {
334
+ //let st = 1.1 + (Math.sin(performance.now() / 1000) * .5);
335
+ //slugMesh.material.map.repeat.set(st, st)
336
+ }
337
+
338
+ const glitchCheckbox = document.getElementById('matrixGlitch');
339
+ if (glitchCheckbox && glitchCheckbox.checked && loadedData) {
340
+ if (Math.random() < 0.25) { // 25% chance per frame to glitch
341
+ let currentText = document.getElementById('textInput').value.split('');
342
+ let numGlitches = Math.floor(Math.random() * 1800) + 1;
343
+ const keys = Array.from(loadedData.codePoints.keys()).filter(k => k > 32); // Exclude space and hidden ones
344
+
345
+ for (let i = 0; i < numGlitches; i++) {
346
+ let idx = Math.floor(Math.random() * currentText.length);
347
+ if (currentText[idx] !== '\n' && currentText[idx] !== ' ') {
348
+ let randomChar = String.fromCodePoint(keys[Math.floor(Math.random() * keys.length)]);
349
+ currentText[idx] = randomChar;
350
+ }
351
+ }
352
+
353
+ slugMesh.geometry.clear();
354
+ slugMesh.geometry.addText(currentText.join(''), loadedData, {
355
+ fontScale: 0.5,
356
+ startX: 0,
357
+ startY: 0,
358
+ justify: document.getElementById('textJustify').value
359
+ });
360
+ slugMesh.userData.isGlitched = true;
361
+ }
362
+ } else if (slugMesh.userData.isGlitched) {
363
+ // Restore original clean text immediately when unchecked
364
+ slugMesh.geometry.clear();
365
+ slugMesh.geometry.addText(document.getElementById('textInput').value, loadedData, {
366
+ fontScale: 0.5,
367
+ startX: 0,
368
+ startY: 0,
369
+ justify: document.getElementById('textJustify').value
370
+ });
371
+ slugMesh.userData.isGlitched = false;
372
+ }
373
+ }
374
+
375
+ if (debugCube) {
376
+ // Keep debug cube visible in frame and spinning wildly
377
+ debugCube.position.y = 50;
378
+ debugCube.position.x = Math.sin(Date.now() * 0.001) * 300;
379
+ debugCube.rotation.x += 0.01;
380
+ debugCube.rotation.y += 0.02;
381
+
382
+ if (pointLight) {
383
+ pointLight.position.x = debugCube.position.x + Math.cos(Date.now() * 0.0015) * 300;
384
+ pointLight.position.y = debugCube.position.y + Math.sin(Date.now() * 0.0015) * 300;
385
+ pointLight.position.z = debugCube.position.z + Math.cos(Date.now() * 0.001) * 150;
386
+ }
387
+ }
388
+
389
+ if (controls) controls.update();
390
+ renderer.render(scene, camera);
391
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "opentype.js": "^1.3.4"
4
4
  },
5
5
  "name": "three-slug",
6
- "version": "1.0.0",
6
+ "version": "1.1.1",
7
7
  "description": "JSlug is a Javascript and WebGL port of Eric Lengyel's **Slug** font rendering algorithm, implemented for **Three.js**.",
8
8
  "main": "src/index.js",
9
9
  "module": "src/index.js",
@@ -138,6 +138,18 @@ flat out uvec4 vBandMaxTexCoords;
138
138
  const slug_vertex = `
139
139
  vec3 transformed = vec3( position.xy * aScaleBias.xy + aScaleBias.zw, 0.0 );
140
140
  vTexCoords = position.xy * 0.5 + 0.5;
141
+
142
+ #ifdef SLUG_MODELSPACE_UV
143
+ #ifdef USE_UV
144
+ vUv = transformed.xy;
145
+ #endif
146
+ #ifdef USE_MAP
147
+
148
+
149
+ vMapUv = ( mapTransform * vec3( vec2(length(modelMatrix[0].xyz)*transformed.x,length(modelMatrix[2].xyz)*transformed.y), 1.0 ) ).xy;
150
+ #endif
151
+ #endif
152
+
141
153
  vGlyphBandScale = aGlyphBandScale;
142
154
  vBandMaxTexCoords = uvec4(aBandMaxTexCoords);
143
155
  `;
@@ -157,7 +169,7 @@ export function injectSlug(target, ...args) {
157
169
  slugData._depthMaterial = new THREE.MeshDepthMaterial({ side: THREE.DoubleSide });
158
170
  injectSlug(slugData._depthMaterial, slugData);
159
171
  }
160
-
172
+
161
173
  if (!slugData._distanceMaterial) {
162
174
  slugData._distanceMaterial = new THREE.MeshDistanceMaterial({ side: THREE.DoubleSide });
163
175
  injectSlug(slugData._distanceMaterial, slugData);
@@ -173,9 +185,9 @@ export function injectSlug(target, ...args) {
173
185
  const slugData = args[0];
174
186
 
175
187
  if (material.userData && material.userData.slugInjected) return; // Prevent redundant native macro splicing
176
-
188
+
177
189
  material.transparent = true;
178
- material.alphaTest = 0.01;
190
+ material.alphaTest = 0.01;
179
191
 
180
192
  material.onBeforeCompile = (shader) => {
181
193
  shader.uniforms.curvesTex = { value: slugData.curvesTex };