three-slug 1.1.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.
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,383 +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
- // Create a procedural grid texture to test local UV-mapping layout support
233
- const canvas = document.createElement('canvas');
234
- canvas.width = 128;
235
- canvas.height = 128;
236
- const ctx = canvas.getContext('2d');
237
-
238
- // Draw highly contrasting 2x2 Checkerboard
239
- ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, 128, 128);
240
- ctx.fillStyle = '#ee0088'; // Vibrant magenta for clear grid inspection
241
- ctx.fillRect(0, 0, 64, 64);
242
- ctx.fillRect(64, 64, 64, 64);
243
-
244
- const gridTex = new THREE.CanvasTexture(canvas);
245
- gridTex.wrapS = THREE.RepeatWrapping;
246
- gridTex.wrapT = THREE.RepeatWrapping;
247
- gridTex.repeat.set(.5, .5);
248
- gridTex.magFilter = THREE.NearestFilter;
249
- gridTex.minFilter = THREE.NearestFilter;
250
-
251
-
252
-
253
- const textToRender = document.getElementById('textInput').value;
254
- const justifyMode = document.getElementById('textJustify').value;
255
-
256
- // Dynamically size the geometry capacity to exactly the number of characters
257
- // Add a minimum of 1 to avoid Three.js throwing empty buffer errors
258
- const geometry = new SlugGeometry(Math.max(1, textToRender.length));
259
-
260
- geometry.clear();
261
-
262
- geometry.addText(textToRender, loadedData, {
263
- fontScale: 0.5,
264
- startX: 0,
265
- startY: 0,
266
- justify: justifyMode
267
- });
268
-
269
- if (useRawFallback) {
270
- material = new SlugMaterial({
271
- curvesTex: loadedData.curvesTex,
272
- bandsTex: loadedData.bandsTex
273
- });
274
- // No custom shadow material for raw shader fallback
275
- } else {
276
- material = new THREE.MeshStandardMaterial({
277
- color: 0xffffff,//0x00aaff, // Golden yellow
278
- roughness: 0.1, // Pure diffuse plastic surface to properly scatter un-angled SpotLight luminance
279
- metalness: 0.05, // Removing metalness prevents the flat quads from reflecting the void into a dark mirror
280
- side: THREE.DoubleSide,
281
- map: gridTex
282
- });
283
- material.defines = { SLUG_MODELSPACE_UV: '' };
284
- material.needsUpdate = true;
285
- }
286
-
287
- let newSlugMesh = new THREE.Mesh(geometry, material);
288
- if (slugMesh) {
289
- newSlugMesh.position.copy(slugMesh.position);
290
- }
291
- slugMesh = newSlugMesh;
292
-
293
- // Apply the architectural PBR macros and instantiate Shared Global Shadow maps securely
294
- injectSlug(slugMesh, material, loadedData);
295
-
296
- // Let Three.js dynamically build the Depth Material derived from our onBeforeCompile graph instead of forcing custom
297
- slugMesh.castShadow = true;
298
- slugMesh.receiveShadow = true;
299
-
300
- // Disabling frustum culling temporally to purely isolate WebGL shadow buffer pipelines
301
- //slugMesh.frustumCulled = false;
302
- slugMesh.scale.multiplyScalar(.02);
303
- scene.add(slugMesh);
304
- }
305
-
306
- function onWindowResize() {
307
- camera.aspect = window.innerWidth / window.innerHeight;
308
- camera.updateProjectionMatrix();
309
- renderer.setSize(window.innerWidth, window.innerHeight);
310
- }
311
-
312
- function animate() {
313
- requestAnimationFrame(animate);
314
-
315
- // Star Wars Title Crawl
316
- if (slugMesh) {
317
- const autoScroll = document.getElementById('autoScroll');
318
- if (autoScroll && autoScroll.checked) {
319
- const speed = 1.7;
320
- slugMesh.position.y += speed;
321
- if (slugMesh.position.y > 1500) {
322
- slugMesh.position.y -= 1000;
323
- }
324
- }
325
- if (slugMesh.material.map) {
326
- //let st = 1.1 + (Math.sin(performance.now() / 1000) * .5);
327
- //slugMesh.material.map.repeat.set(st, st)
328
- }
329
-
330
- const glitchCheckbox = document.getElementById('matrixGlitch');
331
- if (glitchCheckbox && glitchCheckbox.checked && loadedData) {
332
- if (Math.random() < 0.25) { // 25% chance per frame to glitch
333
- let currentText = document.getElementById('textInput').value.split('');
334
- let numGlitches = Math.floor(Math.random() * 1800) + 1;
335
- const keys = Array.from(loadedData.codePoints.keys()).filter(k => k > 32); // Exclude space and hidden ones
336
-
337
- for (let i = 0; i < numGlitches; i++) {
338
- let idx = Math.floor(Math.random() * currentText.length);
339
- if (currentText[idx] !== '\n' && currentText[idx] !== ' ') {
340
- let randomChar = String.fromCodePoint(keys[Math.floor(Math.random() * keys.length)]);
341
- currentText[idx] = randomChar;
342
- }
343
- }
344
-
345
- slugMesh.geometry.clear();
346
- slugMesh.geometry.addText(currentText.join(''), loadedData, {
347
- fontScale: 0.5,
348
- startX: 0,
349
- startY: 0,
350
- justify: document.getElementById('textJustify').value
351
- });
352
- slugMesh.userData.isGlitched = true;
353
- }
354
- } else if (slugMesh.userData.isGlitched) {
355
- // Restore original clean text immediately when unchecked
356
- slugMesh.geometry.clear();
357
- slugMesh.geometry.addText(document.getElementById('textInput').value, loadedData, {
358
- fontScale: 0.5,
359
- startX: 0,
360
- startY: 0,
361
- justify: document.getElementById('textJustify').value
362
- });
363
- slugMesh.userData.isGlitched = false;
364
- }
365
- }
366
-
367
- if (debugCube) {
368
- // Keep debug cube visible in frame and spinning wildly
369
- debugCube.position.y = 50;
370
- debugCube.position.x = Math.sin(Date.now() * 0.001) * 300;
371
- debugCube.rotation.x += 0.01;
372
- debugCube.rotation.y += 0.02;
373
-
374
- if (pointLight) {
375
- pointLight.position.x = debugCube.position.x + Math.cos(Date.now() * 0.0015) * 300;
376
- pointLight.position.y = debugCube.position.y + Math.sin(Date.now() * 0.0015) * 300;
377
- pointLight.position.z = debugCube.position.z + Math.cos(Date.now() * 0.001) * 150;
378
- }
379
- }
380
-
381
- if (controls) controls.update();
382
- renderer.render(scene, camera);
383
- }
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.1.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",