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 +4 -0
- package/demo/fonts/Barbarian.ttf +0 -0
- package/demo/fonts/fonts.json +3 -2
- package/demo/index.html +2 -1
- package/demo/main.js +391 -357
- package/package.json +1 -1
- package/src/SlugMaterial.js +15 -3
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
|

|
|
Binary file
|
package/demo/fonts/fonts.json
CHANGED
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"
|
|
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 =
|
|
54
|
-
spotLight.shadow.mapSize.height =
|
|
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('
|
|
112
|
-
if (loadedData) createTextMesh();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
a.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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.
|
|
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",
|
package/src/SlugMaterial.js
CHANGED
|
@@ -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 };
|