@wodira/browser 0.1.0 → 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/CHANGELOG.md +30 -0
- package/README.md +259 -17
- package/dist/index.cjs +38 -5
- package/dist/index.d.ts +110 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -4
- package/dist/types.d.ts +56 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/boxes/cdn.d.ts +2 -0
- package/dist/ui/boxes/cdn.d.ts.map +1 -0
- package/dist/ui/boxes/cdn.js +743 -0
- package/dist/ui/boxes/client.d.ts +30 -0
- package/dist/ui/boxes/client.d.ts.map +1 -0
- package/dist/ui/boxes/components.d.ts +246 -0
- package/dist/ui/boxes/components.d.ts.map +1 -0
- package/dist/ui/boxes/i18n.d.ts +107 -0
- package/dist/ui/boxes/i18n.d.ts.map +1 -0
- package/dist/ui/boxes/index.d.ts +16 -0
- package/dist/ui/boxes/index.d.ts.map +1 -0
- package/dist/ui/boxes/index.js +3071 -0
- package/dist/ui/boxes/styles.d.ts +2 -0
- package/dist/ui/boxes/styles.d.ts.map +1 -0
- package/dist/ui/boxes/types.d.ts +105 -0
- package/dist/ui/boxes/types.d.ts.map +1 -0
- package/dist/ui/components.d.ts +158 -4
- package/dist/ui/components.d.ts.map +1 -1
- package/dist/ui/events.d.ts +1 -1
- package/dist/ui/events.d.ts.map +1 -1
- package/dist/ui/i18n.d.ts +4 -1
- package/dist/ui/i18n.d.ts.map +1 -1
- package/dist/ui/index-tfej862z.js +3264 -0
- package/dist/ui/index-xdhg885t.js +997 -0
- package/dist/ui/index.d.ts +19 -3
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +9038 -2030
- package/dist/ui/layout-styles.d.ts +3 -0
- package/dist/ui/layout-styles.d.ts.map +1 -0
- package/dist/ui/registration-eligibility.d.ts +52 -0
- package/dist/ui/registration-eligibility.d.ts.map +1 -0
- package/dist/ui/shirt-3d-renderer-jyks1h8f.js +10 -0
- package/dist/ui/shirt-sponsors/cdn.d.ts +2 -0
- package/dist/ui/shirt-sponsors/cdn.d.ts.map +1 -0
- package/dist/ui/shirt-sponsors/cdn.js +5125 -0
- package/dist/ui/shirt-sponsors/index.d.ts +9 -0
- package/dist/ui/shirt-sponsors/index.d.ts.map +1 -0
- package/dist/ui/shirt-sponsors/index.js +10 -0
- package/dist/ui/shirt-sponsors/render/auction-model.d.ts +55 -0
- package/dist/ui/shirt-sponsors/render/auction-model.d.ts.map +1 -0
- package/dist/ui/shirt-sponsors/render/index.d.ts +7 -0
- package/dist/ui/shirt-sponsors/render/index.d.ts.map +1 -0
- package/dist/ui/shirt-sponsors/render/index.js +34 -0
- package/dist/ui/shirt-sponsors/render/shirt-3d-renderer.d.ts +109 -0
- package/dist/ui/shirt-sponsors/render/shirt-3d-renderer.d.ts.map +1 -0
- package/dist/ui/shirt-sponsors/render/shirt-artwork-projection.d.ts +13 -0
- package/dist/ui/shirt-sponsors/render/shirt-artwork-projection.d.ts.map +1 -0
- package/dist/ui/shirt-sponsors/render/shirt-artwork.d.ts +17 -0
- package/dist/ui/shirt-sponsors/render/shirt-artwork.d.ts.map +1 -0
- package/dist/ui/shirt-sponsors/render/shirt-cloth-motion.d.ts +31 -0
- package/dist/ui/shirt-sponsors/render/shirt-cloth-motion.d.ts.map +1 -0
- package/dist/ui/shirt-sponsors/render/shirt-silhouette.d.ts +9 -0
- package/dist/ui/shirt-sponsors/render/shirt-silhouette.d.ts.map +1 -0
- package/dist/ui/shirt-sponsors/shirt-auction.d.ts +137 -0
- package/dist/ui/shirt-sponsors/shirt-auction.d.ts.map +1 -0
- package/dist/ui/signature-pad.d.ts +29 -0
- package/dist/ui/signature-pad.d.ts.map +1 -0
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/validators.d.ts +1 -1
- package/dist/ui/validators.d.ts.map +1 -1
- package/package.json +42 -13
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
// src/ui/shirt-sponsors/render/auction-model.ts
|
|
2
|
+
var leader2 = (slot) => slot.bids.find((bid) => bid.status === "leading");
|
|
3
|
+
var minimumBid2 = (slot) => {
|
|
4
|
+
const current = leader2(slot);
|
|
5
|
+
return current ? current.amount + slot.increment : slot.startingPrice;
|
|
6
|
+
};
|
|
7
|
+
var hasPaidBids2 = (campaign) => campaign.slots.some((slot) => slot.bids.length > 0);
|
|
8
|
+
function reserveBid2(campaign, slotId, buyer, amount, now) {
|
|
9
|
+
if (!campaign.published || now >= campaign.closesAt)
|
|
10
|
+
throw new Error("La subasta está cerrada.");
|
|
11
|
+
const slot = campaign.slots.find((item) => item.id === slotId);
|
|
12
|
+
if (!slot)
|
|
13
|
+
throw new Error("Selecciona un espacio.");
|
|
14
|
+
if (!buyer.trim())
|
|
15
|
+
throw new Error("Escribe el nombre del anunciante.");
|
|
16
|
+
if (slot.lock && slot.lock.expiresAt > now)
|
|
17
|
+
throw new Error("Hay un pago en curso.");
|
|
18
|
+
if (leader2(slot)?.buyer === buyer.trim())
|
|
19
|
+
throw new Error("Ya lideras este espacio.");
|
|
20
|
+
if (!Number.isSafeInteger(amount) || amount < minimumBid2(slot))
|
|
21
|
+
throw new Error("La puja no alcanza el mínimo.");
|
|
22
|
+
return {
|
|
23
|
+
...campaign,
|
|
24
|
+
slots: campaign.slots.map((item) => item.id === slotId ? { ...item, lock: { buyer: buyer.trim(), amount, expiresAt: now + 5 * 60000 } } : item)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function settleBid2(campaign, slotId, outcome, now, id) {
|
|
28
|
+
const slot = campaign.slots.find((item) => item.id === slotId);
|
|
29
|
+
if (!slot?.lock)
|
|
30
|
+
throw new Error("No hay un pago pendiente.");
|
|
31
|
+
if (slot.lock.expiresAt <= now)
|
|
32
|
+
throw new Error("El turno ha caducado. Un cobro tardío se devolvería.");
|
|
33
|
+
const paid = outcome === "paid";
|
|
34
|
+
const late = paid && campaign.closesAt - now <= 2 * 60000;
|
|
35
|
+
const closesAt = late ? Math.min(Math.max(campaign.closesAt, now + 2 * 60000), campaign.originalClose + 10 * 60000) : campaign.closesAt;
|
|
36
|
+
const bid = {
|
|
37
|
+
id,
|
|
38
|
+
buyer: slot.lock.buyer,
|
|
39
|
+
amount: slot.lock.amount,
|
|
40
|
+
paidAt: now,
|
|
41
|
+
status: "leading",
|
|
42
|
+
views: 0,
|
|
43
|
+
opens: 0,
|
|
44
|
+
clicks: 0
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
...campaign,
|
|
48
|
+
closesAt,
|
|
49
|
+
slots: campaign.slots.map((item) => item.id === slotId ? {
|
|
50
|
+
...item,
|
|
51
|
+
lock: undefined,
|
|
52
|
+
bids: paid ? [...item.bids.map((old) => ({ ...old, status: "refunded" })), bid] : item.bids
|
|
53
|
+
} : item)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function demoCampaign2(now) {
|
|
57
|
+
const closesAt = now + 24 * 60 * 60000;
|
|
58
|
+
return {
|
|
59
|
+
title: "Carrera híbrida · Demo",
|
|
60
|
+
front: "/images/sponsor-studio/white-race-shirt-front-v1.png",
|
|
61
|
+
back: "/images/sponsor-studio/white-race-shirt-back-v1.png",
|
|
62
|
+
quantity: 500,
|
|
63
|
+
closesAt,
|
|
64
|
+
originalClose: closesAt,
|
|
65
|
+
published: false,
|
|
66
|
+
slots: [
|
|
67
|
+
{
|
|
68
|
+
id: "chest",
|
|
69
|
+
name: "Pecho principal",
|
|
70
|
+
side: "front",
|
|
71
|
+
x: 32,
|
|
72
|
+
y: 29,
|
|
73
|
+
width: 36,
|
|
74
|
+
height: 18,
|
|
75
|
+
printWidth: 24,
|
|
76
|
+
printHeight: 12,
|
|
77
|
+
startingPrice: 1e4,
|
|
78
|
+
increment: 1000,
|
|
79
|
+
bids: []
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "back",
|
|
83
|
+
name: "Espalda",
|
|
84
|
+
side: "back",
|
|
85
|
+
x: 30,
|
|
86
|
+
y: 29,
|
|
87
|
+
width: 40,
|
|
88
|
+
height: 24,
|
|
89
|
+
printWidth: 28,
|
|
90
|
+
printHeight: 18,
|
|
91
|
+
startingPrice: 15000,
|
|
92
|
+
increment: 1000,
|
|
93
|
+
bids: []
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/ui/shirt-sponsors/render/shirt-3d-renderer.ts
|
|
100
|
+
import * as THREE3 from "three";
|
|
101
|
+
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
102
|
+
|
|
103
|
+
// src/ui/shirt-sponsors/render/shirt-cloth-motion.ts
|
|
104
|
+
import * as THREE from "three";
|
|
105
|
+
var clamp = (value, limit) => Math.max(-limit, Math.min(limit, value));
|
|
106
|
+
|
|
107
|
+
class ShirtClothMotion2 {
|
|
108
|
+
yaw = { offset: 0, velocity: 0 };
|
|
109
|
+
pitch = { offset: 0, velocity: 0 };
|
|
110
|
+
flutter = { offset: 0, velocity: 0 };
|
|
111
|
+
turn(yaw, pitch) {
|
|
112
|
+
if (!Number.isFinite(yaw) || !Number.isFinite(pitch))
|
|
113
|
+
return;
|
|
114
|
+
this.yaw.velocity = clamp(this.yaw.velocity - clamp(yaw, 1) * 5.5, 9);
|
|
115
|
+
this.pitch.velocity = clamp(this.pitch.velocity - clamp(pitch, 1) * 4, 7);
|
|
116
|
+
this.flutter.velocity = clamp(this.flutter.velocity + Math.min(Math.hypot(yaw, pitch), 1) * 3, 6);
|
|
117
|
+
}
|
|
118
|
+
advance(seconds) {
|
|
119
|
+
if (!Number.isFinite(seconds) || seconds < 0 || seconds > 0.25) {
|
|
120
|
+
this.reset();
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
let remaining = seconds;
|
|
124
|
+
while (remaining > 0) {
|
|
125
|
+
const dt = Math.min(remaining, 1 / 120);
|
|
126
|
+
this.integrate(this.yaw, dt, 75, 9, 0.34);
|
|
127
|
+
this.integrate(this.pitch, dt, 90, 10, 0.24);
|
|
128
|
+
this.integrate(this.flutter, dt, 220, 8, 0.22);
|
|
129
|
+
remaining -= dt;
|
|
130
|
+
}
|
|
131
|
+
const active = [this.yaw, this.pitch, this.flutter].some((spring) => Math.abs(spring.offset) > 0.0001 || Math.abs(spring.velocity) > 0.001);
|
|
132
|
+
if (!active)
|
|
133
|
+
this.reset();
|
|
134
|
+
return active;
|
|
135
|
+
}
|
|
136
|
+
reset() {
|
|
137
|
+
for (const spring of [this.yaw, this.pitch, this.flutter]) {
|
|
138
|
+
spring.offset = 0;
|
|
139
|
+
spring.velocity = 0;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
integrate(spring, dt, stiffness, damping, limit) {
|
|
143
|
+
spring.velocity += (-stiffness * spring.offset - damping * spring.velocity) * dt;
|
|
144
|
+
spring.offset += spring.velocity * dt;
|
|
145
|
+
if (Math.abs(spring.offset) > limit) {
|
|
146
|
+
spring.offset = clamp(spring.offset, limit);
|
|
147
|
+
spring.velocity = 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
class ShirtClothMesh2 {
|
|
153
|
+
geometry;
|
|
154
|
+
rest;
|
|
155
|
+
restNormals;
|
|
156
|
+
normalCorrections;
|
|
157
|
+
weights;
|
|
158
|
+
ripples;
|
|
159
|
+
height;
|
|
160
|
+
deformed = false;
|
|
161
|
+
constructor(geometry) {
|
|
162
|
+
this.geometry = geometry;
|
|
163
|
+
const positions = geometry.getAttribute("position");
|
|
164
|
+
if (!geometry.getAttribute("normal"))
|
|
165
|
+
geometry.computeVertexNormals();
|
|
166
|
+
const normals = geometry.getAttribute("normal");
|
|
167
|
+
this.rest = new Float32Array(positions.count * 3);
|
|
168
|
+
this.restNormals = new Float32Array(normals.count * 3);
|
|
169
|
+
this.normalCorrections = new Float32Array(normals.count * 3);
|
|
170
|
+
this.weights = new Float32Array(positions.count);
|
|
171
|
+
this.ripples = new Float32Array(positions.count);
|
|
172
|
+
geometry.computeBoundingBox();
|
|
173
|
+
geometry.computeBoundingSphere();
|
|
174
|
+
const bounds = geometry.boundingBox;
|
|
175
|
+
this.height = Math.max(bounds.max.y - bounds.min.y, 0.001);
|
|
176
|
+
const halfWidth = Math.max(Math.abs(bounds.min.x), Math.abs(bounds.max.x), 0.001);
|
|
177
|
+
for (let i = 0;i < positions.count; i++) {
|
|
178
|
+
const x = positions.getX(i), y = positions.getY(i), z = positions.getZ(i);
|
|
179
|
+
this.rest.set([x, y, z], i * 3);
|
|
180
|
+
this.restNormals.set([normals.getX(i), normals.getY(i), normals.getZ(i)], i * 3);
|
|
181
|
+
const drop = (bounds.max.y - y) / this.height;
|
|
182
|
+
const hem = THREE.MathUtils.smoothstep(drop, 0.25, 1);
|
|
183
|
+
const sleeve = THREE.MathUtils.smoothstep(Math.abs(x) / halfWidth, 0.58, 1) * THREE.MathUtils.smoothstep(drop, 0.06, 0.28);
|
|
184
|
+
this.weights[i] = Math.max(hem * hem, sleeve * 0.8);
|
|
185
|
+
this.ripples[i] = Math.sin(x / halfWidth * 8 + drop * 5) * (hem ** 3 + sleeve * 0.7);
|
|
186
|
+
}
|
|
187
|
+
geometry.computeVertexNormals();
|
|
188
|
+
for (let i = 0;i < normals.count; i++) {
|
|
189
|
+
this.normalCorrections[i * 3] = this.restNormals[i * 3] - normals.getX(i);
|
|
190
|
+
this.normalCorrections[i * 3 + 1] = this.restNormals[i * 3 + 1] - normals.getY(i);
|
|
191
|
+
this.normalCorrections[i * 3 + 2] = this.restNormals[i * 3 + 2] - normals.getZ(i);
|
|
192
|
+
normals.setXYZ(i, this.restNormals[i * 3], this.restNormals[i * 3 + 1], this.restNormals[i * 3 + 2]);
|
|
193
|
+
}
|
|
194
|
+
geometry.boundingBox.expandByScalar(this.height * 0.35);
|
|
195
|
+
geometry.boundingSphere.radius += this.height * 0.35;
|
|
196
|
+
if (positions instanceof THREE.BufferAttribute)
|
|
197
|
+
positions.setUsage(THREE.DynamicDrawUsage);
|
|
198
|
+
if (normals instanceof THREE.BufferAttribute)
|
|
199
|
+
normals.setUsage(THREE.DynamicDrawUsage);
|
|
200
|
+
}
|
|
201
|
+
apply(motion) {
|
|
202
|
+
const positions = this.geometry.getAttribute("position");
|
|
203
|
+
for (let i = 0;i < positions.count; i++) {
|
|
204
|
+
const x = this.rest[i * 3], y = this.rest[i * 3 + 1], z = this.rest[i * 3 + 2];
|
|
205
|
+
const weight = this.weights[i];
|
|
206
|
+
const twist = motion.yaw.offset * weight;
|
|
207
|
+
const cos = Math.cos(twist), sin = Math.sin(twist);
|
|
208
|
+
const ripple = motion.flutter.offset * this.height * 0.13 * this.ripples[i];
|
|
209
|
+
positions.setXYZ(i, x * cos + z * sin + motion.yaw.offset * this.height * 0.12 * weight, y + Math.abs(twist) * this.height * 0.025 * weight + ripple * 0.25, z * cos - x * sin + motion.pitch.offset * this.height * 0.4 * weight + ripple);
|
|
210
|
+
}
|
|
211
|
+
positions.needsUpdate = true;
|
|
212
|
+
this.geometry.computeVertexNormals();
|
|
213
|
+
const normals = this.geometry.getAttribute("normal");
|
|
214
|
+
for (let i = 0;i < normals.count; i++) {
|
|
215
|
+
const x = normals.getX(i) + this.normalCorrections[i * 3];
|
|
216
|
+
const y = normals.getY(i) + this.normalCorrections[i * 3 + 1];
|
|
217
|
+
const z = normals.getZ(i) + this.normalCorrections[i * 3 + 2];
|
|
218
|
+
const length = Math.hypot(x, y, z) || 1;
|
|
219
|
+
normals.setXYZ(i, x / length, y / length, z / length);
|
|
220
|
+
}
|
|
221
|
+
this.deformed = true;
|
|
222
|
+
}
|
|
223
|
+
restore() {
|
|
224
|
+
if (!this.deformed)
|
|
225
|
+
return;
|
|
226
|
+
const positions = this.geometry.getAttribute("position");
|
|
227
|
+
const normals = this.geometry.getAttribute("normal");
|
|
228
|
+
for (let i = 0;i < positions.count; i++) {
|
|
229
|
+
positions.setXYZ(i, this.rest[i * 3], this.rest[i * 3 + 1], this.rest[i * 3 + 2]);
|
|
230
|
+
normals.setXYZ(i, this.restNormals[i * 3], this.restNormals[i * 3 + 1], this.restNormals[i * 3 + 2]);
|
|
231
|
+
}
|
|
232
|
+
positions.needsUpdate = normals.needsUpdate = true;
|
|
233
|
+
this.deformed = false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/ui/shirt-sponsors/render/shirt-artwork-projection.ts
|
|
238
|
+
import * as THREE2 from "three";
|
|
239
|
+
|
|
240
|
+
// src/ui/shirt-sponsors/render/shirt-silhouette.ts
|
|
241
|
+
function shirtArmhole2(spans) {
|
|
242
|
+
const radius = Math.max(1, Math.round(spans.length * 0.02));
|
|
243
|
+
let best = 0;
|
|
244
|
+
let row = Math.round(spans.length * 0.42);
|
|
245
|
+
for (let i = Math.round(spans.length * 0.22);i < spans.length * 0.6; i++) {
|
|
246
|
+
const above = spans[Math.max(0, i - radius)];
|
|
247
|
+
const below = spans[Math.min(spans.length - 1, i + radius)];
|
|
248
|
+
const reduction = above.right - above.left - (below.right - below.left);
|
|
249
|
+
if (reduction > best) {
|
|
250
|
+
best = reduction;
|
|
251
|
+
row = i;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return row / (spans.length - 1);
|
|
255
|
+
}
|
|
256
|
+
function mapShirtHeight2(y, fromArmhole, toArmhole) {
|
|
257
|
+
return y <= fromArmhole ? y / fromArmhole * toArmhole : toArmhole + (y - fromArmhole) / (1 - fromArmhole) * (1 - toArmhole);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/ui/shirt-sponsors/render/shirt-artwork.ts
|
|
261
|
+
function paintShirtArtwork2(context, artwork, images, width, height, silhouette = []) {
|
|
262
|
+
context.clearRect(0, 0, width * 2, height);
|
|
263
|
+
for (const [index, side] of ["front", "back"].entries()) {
|
|
264
|
+
const layer = artwork?.[side];
|
|
265
|
+
const image = layer && images.get(layer.url);
|
|
266
|
+
if (!layer || !image?.complete || !image.naturalWidth)
|
|
267
|
+
continue;
|
|
268
|
+
const scale = Math.min(width / image.naturalWidth, height / image.naturalHeight) * layer.scale;
|
|
269
|
+
const w = image.naturalWidth * scale;
|
|
270
|
+
const h = image.naturalHeight * scale;
|
|
271
|
+
context.save();
|
|
272
|
+
context.beginPath();
|
|
273
|
+
context.rect(index * width, 0, width, height);
|
|
274
|
+
context.clip();
|
|
275
|
+
if (layer.photo && silhouette.length) {
|
|
276
|
+
context.translate(index * width + width / 2 + layer.x * width / 100, height / 2 + layer.y * height / 100);
|
|
277
|
+
context.scale(layer.scale, layer.scale);
|
|
278
|
+
const armhole = shirtArmhole2(silhouette);
|
|
279
|
+
const torso = silhouette[Math.round(silhouette.length * 0.58)];
|
|
280
|
+
for (let row = 0;row < silhouette.length; row++) {
|
|
281
|
+
const span = silhouette[row];
|
|
282
|
+
const left = side === "front" ? span.left : 1 - span.right;
|
|
283
|
+
const right = side === "front" ? span.right : 1 - span.left;
|
|
284
|
+
const y = row / silhouette.length;
|
|
285
|
+
const torsoLeft = side === "front" ? torso.left : 1 - torso.right;
|
|
286
|
+
const torsoRight = side === "front" ? torso.right : 1 - torso.left;
|
|
287
|
+
const strips = [
|
|
288
|
+
[0, 0.25, left, torsoLeft],
|
|
289
|
+
[0.25, 0.5, torsoLeft, torsoRight],
|
|
290
|
+
[0.75, 0.25, torsoRight, right]
|
|
291
|
+
];
|
|
292
|
+
for (const [sx, sw, from, to] of strips) {
|
|
293
|
+
if (to <= from)
|
|
294
|
+
continue;
|
|
295
|
+
context.drawImage(image, sx * image.naturalWidth, mapShirtHeight2(y, armhole, 0.42) * (image.naturalHeight - 1), sw * image.naturalWidth, image.naturalHeight / silhouette.length, (from - 0.5) * width - 2, (y - 0.5) * height, (to - from) * width + 4, height / silhouette.length + 1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
context.restore();
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
context.drawImage(image, index * width + (width - w) / 2 + layer.x * width / 100, (height - h) / 2 + layer.y * height / 100, w, h);
|
|
302
|
+
context.restore();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/ui/shirt-sponsors/render/shirt-artwork-projection.ts
|
|
307
|
+
class ShirtArtworkProjection2 {
|
|
308
|
+
silhouette = [];
|
|
309
|
+
canvas = document.createElement("canvas");
|
|
310
|
+
texture = new THREE2.CanvasTexture(this.canvas);
|
|
311
|
+
uniforms = {
|
|
312
|
+
wodiraArtworkMap: { value: this.texture },
|
|
313
|
+
wodiraArtworkEnabled: { value: false },
|
|
314
|
+
wodiraFrontFabricColor: { value: new THREE2.Color },
|
|
315
|
+
wodiraBackFabricColor: { value: new THREE2.Color }
|
|
316
|
+
};
|
|
317
|
+
constructor(model) {
|
|
318
|
+
model.updateMatrixWorld(true);
|
|
319
|
+
const inverseRoot = model.matrixWorld.clone().invert();
|
|
320
|
+
const bounds = new THREE2.Box3;
|
|
321
|
+
const meshes = [];
|
|
322
|
+
model.traverse((object) => {
|
|
323
|
+
if (!(object instanceof THREE2.Mesh))
|
|
324
|
+
return;
|
|
325
|
+
const attribute = object.geometry.getAttribute("position");
|
|
326
|
+
const transform = inverseRoot.clone().multiply(object.matrixWorld);
|
|
327
|
+
if (!object.geometry.hasAttribute("normal"))
|
|
328
|
+
object.geometry.computeVertexNormals();
|
|
329
|
+
const normals = object.geometry.getAttribute("normal");
|
|
330
|
+
const normalTransform = new THREE2.Matrix3().getNormalMatrix(transform);
|
|
331
|
+
const facing = Array.from({ length: normals.count }, (_, index) => new THREE2.Vector3().fromBufferAttribute(normals, index).applyNormalMatrix(normalTransform).z);
|
|
332
|
+
const positions = Array.from({ length: attribute.count }, (_, index) => {
|
|
333
|
+
const point = new THREE2.Vector3().fromBufferAttribute(attribute, index).applyMatrix4(transform);
|
|
334
|
+
bounds.expandByPoint(point);
|
|
335
|
+
return point;
|
|
336
|
+
});
|
|
337
|
+
meshes.push({ mesh: object, positions, facing });
|
|
338
|
+
});
|
|
339
|
+
const size = bounds.getSize(new THREE2.Vector3);
|
|
340
|
+
const resolution = 512;
|
|
341
|
+
this.silhouette = Array.from({ length: resolution }, () => ({ left: 1, right: 0 }));
|
|
342
|
+
for (const { mesh, positions } of meshes) {
|
|
343
|
+
const indices = mesh.geometry.index;
|
|
344
|
+
const count = indices?.count ?? positions.length;
|
|
345
|
+
for (let i = 0;i < count; i += 3) {
|
|
346
|
+
const triangle = [0, 1, 2].map((j) => positions[indices ? indices.getX(i + j) : i + j]);
|
|
347
|
+
for (let edge = 0;edge < 3; edge++) {
|
|
348
|
+
const a = triangle[edge];
|
|
349
|
+
const b = triangle[(edge + 1) % 3];
|
|
350
|
+
const ay = (bounds.max.y - a.y) / size.y * resolution;
|
|
351
|
+
const by = (bounds.max.y - b.y) / size.y * resolution;
|
|
352
|
+
if (Math.abs(by - ay) < 0.00001)
|
|
353
|
+
continue;
|
|
354
|
+
const start = Math.max(0, Math.ceil(Math.min(ay, by) - 0.5));
|
|
355
|
+
const end = Math.min(resolution - 1, Math.floor(Math.max(ay, by) - 0.5));
|
|
356
|
+
for (let row = start;row <= end; row++) {
|
|
357
|
+
const t = (row + 0.5 - ay) / (by - ay);
|
|
358
|
+
const x = (a.x + (b.x - a.x) * t - bounds.min.x) / size.x;
|
|
359
|
+
const span = this.silhouette[row];
|
|
360
|
+
span.left = Math.min(span.left, x);
|
|
361
|
+
span.right = Math.max(span.right, x);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
for (let row = 0;row < resolution; row++) {
|
|
367
|
+
if (this.silhouette[row].right <= this.silhouette[row].left)
|
|
368
|
+
this.silhouette[row] = { ...this.silhouette[row - 1] ?? { left: 0, right: 1 } };
|
|
369
|
+
}
|
|
370
|
+
this.canvas.width = 2048;
|
|
371
|
+
this.canvas.height = Math.min(2048, Math.max(1, Math.round(1024 * size.y / size.x)));
|
|
372
|
+
this.texture.colorSpace = THREE2.SRGBColorSpace;
|
|
373
|
+
this.texture.flipY = false;
|
|
374
|
+
this.texture.generateMipmaps = false;
|
|
375
|
+
this.texture.minFilter = THREE2.LinearFilter;
|
|
376
|
+
for (const { mesh, positions, facing } of meshes) {
|
|
377
|
+
const uv = new Float32Array(positions.length * 3);
|
|
378
|
+
positions.forEach((point, index) => {
|
|
379
|
+
uv[index * 3] = (point.x - bounds.min.x) / size.x;
|
|
380
|
+
uv[index * 3 + 1] = (bounds.max.y - point.y) / size.y;
|
|
381
|
+
uv[index * 3 + 2] = facing[index];
|
|
382
|
+
});
|
|
383
|
+
mesh.geometry.setAttribute("wodiraArtworkUv", new THREE2.BufferAttribute(uv, 3));
|
|
384
|
+
for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
|
|
385
|
+
if (!(material instanceof THREE2.MeshStandardMaterial))
|
|
386
|
+
continue;
|
|
387
|
+
material.onBeforeCompile = (shader) => {
|
|
388
|
+
Object.assign(shader.uniforms, this.uniforms);
|
|
389
|
+
shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>
|
|
390
|
+
attribute vec3 wodiraArtworkUv;
|
|
391
|
+
varying vec3 vWodiraArtworkUv;`).replace("#include <begin_vertex>", `#include <begin_vertex>
|
|
392
|
+
vWodiraArtworkUv = wodiraArtworkUv;`);
|
|
393
|
+
shader.fragmentShader = shader.fragmentShader.replace("#include <common>", `#include <common>
|
|
394
|
+
uniform sampler2D wodiraArtworkMap;
|
|
395
|
+
uniform bool wodiraArtworkEnabled;
|
|
396
|
+
uniform vec3 wodiraFrontFabricColor;
|
|
397
|
+
uniform vec3 wodiraBackFabricColor;
|
|
398
|
+
varying vec3 vWodiraArtworkUv;`).replace("#include <map_fragment>", `
|
|
399
|
+
// The authored atlas partitions sleeves per triangle. Continue the torso
|
|
400
|
+
// print across that seam instead of clipping it to a jagged UV island.
|
|
401
|
+
bool front = vWodiraArtworkUv.z >= 0.0;
|
|
402
|
+
float torsoU = front ? vWodiraArtworkUv.x * 0.25 : 0.5 - vWodiraArtworkUv.x * 0.25;
|
|
403
|
+
torsoU = front ? clamp(torsoU, 0.00025, 0.24975) : clamp(torsoU, 0.25025, 0.49975);
|
|
404
|
+
vec4 sponsorColor = texture2D(map, vMapUv);
|
|
405
|
+
if (vMapUv.x >= 0.5) {
|
|
406
|
+
vec4 torsoColor = texture2D(map, vec2(torsoU, vWodiraArtworkUv.y));
|
|
407
|
+
// Sleeve artwork takes priority where its pixels are opaque.
|
|
408
|
+
float alpha = sponsorColor.a + torsoColor.a * (1.0 - sponsorColor.a);
|
|
409
|
+
vec3 color = sponsorColor.rgb * sponsorColor.a
|
|
410
|
+
+ torsoColor.rgb * torsoColor.a * (1.0 - sponsorColor.a);
|
|
411
|
+
sponsorColor = vec4(color / max(alpha, 0.00001), alpha);
|
|
412
|
+
}
|
|
413
|
+
vec3 fabricColor = front ? wodiraFrontFabricColor : wodiraBackFabricColor;
|
|
414
|
+
vec3 clothColor = fabricColor;
|
|
415
|
+
if (wodiraArtworkEnabled) {
|
|
416
|
+
// Mirror the back's horizontal axis so lettering reads from behind.
|
|
417
|
+
float u = vWodiraArtworkUv.z >= 0.0
|
|
418
|
+
? clamp(vWodiraArtworkUv.x * 0.5, 0.00025, 0.49975)
|
|
419
|
+
: clamp(1.0 - vWodiraArtworkUv.x * 0.5, 0.50025, 0.99975);
|
|
420
|
+
vec4 printColor = texture2D(wodiraArtworkMap, vec2(u, vWodiraArtworkUv.y));
|
|
421
|
+
clothColor = mix(fabricColor, printColor.rgb, printColor.a);
|
|
422
|
+
}
|
|
423
|
+
diffuseColor.rgb *= mix(clothColor, sponsorColor.rgb, sponsorColor.a);
|
|
424
|
+
`);
|
|
425
|
+
shader.fragmentShader = shader.fragmentShader.replace("#include <lights_physical_fragment>", `#include <lights_physical_fragment>
|
|
426
|
+
#ifdef USE_SHEEN
|
|
427
|
+
if (wodiraArtworkEnabled) material.sheenColor = 0.3 * mix(diffuseColor.rgb, vec3(1.0), 0.35);
|
|
428
|
+
#endif`);
|
|
429
|
+
};
|
|
430
|
+
material.customProgramCacheKey = () => "wodira-artwork-continuous-sponsors-v3";
|
|
431
|
+
material.needsUpdate = true;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
update(artwork, images, dye) {
|
|
436
|
+
this.uniforms.wodiraArtworkEnabled.value = !!(artwork?.front || artwork?.back);
|
|
437
|
+
const front = artwork?.front?.fabricColor;
|
|
438
|
+
const back = artwork?.back?.fabricColor;
|
|
439
|
+
this.uniforms.wodiraFrontFabricColor.value.copy(front || back ? new THREE2.Color(front ?? back) : dye);
|
|
440
|
+
this.uniforms.wodiraBackFabricColor.value.copy(back || front ? new THREE2.Color(back ?? front) : dye);
|
|
441
|
+
const context = this.canvas.getContext("2d");
|
|
442
|
+
if (context)
|
|
443
|
+
paintShirtArtwork2(context, artwork, images, this.canvas.width / 2, this.canvas.height, this.silhouette);
|
|
444
|
+
this.texture.needsUpdate = true;
|
|
445
|
+
}
|
|
446
|
+
dispose() {
|
|
447
|
+
this.texture.dispose();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/ui/shirt-sponsors/render/shirt-3d-renderer.ts
|
|
452
|
+
var defaultShirt3dPalette2 = {
|
|
453
|
+
slot: "#e32397",
|
|
454
|
+
slotPreview: "#ff1593",
|
|
455
|
+
slotText: "#ffffff",
|
|
456
|
+
priceLeading: "#e40086",
|
|
457
|
+
price: "#101632",
|
|
458
|
+
reserved: "#c9c5d3",
|
|
459
|
+
selected: "#101632"
|
|
460
|
+
};
|
|
461
|
+
var SHIRT_MODEL_PATH2 = "/images/sponsor-studio/white-shirt-realistic-v1.glb?v=7";
|
|
462
|
+
|
|
463
|
+
class Shirt3dRenderer2 {
|
|
464
|
+
canvas;
|
|
465
|
+
fail;
|
|
466
|
+
recovering;
|
|
467
|
+
renderer;
|
|
468
|
+
scene = new THREE3.Scene;
|
|
469
|
+
camera = new THREE3.OrthographicCamera(-1, 1, 1.12, -1.12, 0.1, 20);
|
|
470
|
+
model;
|
|
471
|
+
observer;
|
|
472
|
+
disposed = false;
|
|
473
|
+
loadController = new AbortController;
|
|
474
|
+
contextUnavailable = false;
|
|
475
|
+
recoveryTimer;
|
|
476
|
+
firstFrame;
|
|
477
|
+
frame = 0;
|
|
478
|
+
textureDirty = false;
|
|
479
|
+
atlas = document.createElement("canvas");
|
|
480
|
+
texture = new THREE3.CanvasTexture(this.atlas);
|
|
481
|
+
fabricTexture = this.createFabricTexture();
|
|
482
|
+
fabricMaterials = [];
|
|
483
|
+
images = new Map;
|
|
484
|
+
slots = [];
|
|
485
|
+
rotation = 0;
|
|
486
|
+
tilt = 0;
|
|
487
|
+
side = "front";
|
|
488
|
+
viewSurface;
|
|
489
|
+
rotating = false;
|
|
490
|
+
zoom = 1;
|
|
491
|
+
shirtColor = "#ffffff";
|
|
492
|
+
showBidPrices = false;
|
|
493
|
+
artwork = null;
|
|
494
|
+
artworkProjection;
|
|
495
|
+
priceFormatter;
|
|
496
|
+
palette;
|
|
497
|
+
selectedSlotId = null;
|
|
498
|
+
modelUrl;
|
|
499
|
+
raycaster = new THREE3.Raycaster;
|
|
500
|
+
cloth = new ShirtClothMotion2;
|
|
501
|
+
clothMeshes = [];
|
|
502
|
+
clothActive = false;
|
|
503
|
+
lastFrameTime = 0;
|
|
504
|
+
reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
505
|
+
constructor(canvas, fail, recovering = () => {}, options = {}) {
|
|
506
|
+
this.canvas = canvas;
|
|
507
|
+
this.fail = fail;
|
|
508
|
+
this.recovering = recovering;
|
|
509
|
+
this.modelUrl = new URL(options.modelUrl ?? SHIRT_MODEL_PATH2, document.baseURI);
|
|
510
|
+
this.palette = { ...defaultShirt3dPalette2, ...options.palette };
|
|
511
|
+
this.priceFormatter = this.createPriceFormatter(options.locale);
|
|
512
|
+
this.atlas.width = 2048;
|
|
513
|
+
this.atlas.height = 574;
|
|
514
|
+
this.texture.generateMipmaps = false;
|
|
515
|
+
this.texture.minFilter = THREE3.LinearFilter;
|
|
516
|
+
this.texture.colorSpace = THREE3.SRGBColorSpace;
|
|
517
|
+
this.texture.flipY = false;
|
|
518
|
+
this.renderer = new THREE3.WebGLRenderer({ canvas, alpha: true, antialias: true });
|
|
519
|
+
this.renderer.debug.onShaderError = () => {
|
|
520
|
+
throw new Error("Shirt material shader unavailable");
|
|
521
|
+
};
|
|
522
|
+
this.renderer.toneMapping = THREE3.ACESFilmicToneMapping;
|
|
523
|
+
this.renderer.toneMappingExposure = 1;
|
|
524
|
+
this.renderer.setPixelRatio(Math.min(devicePixelRatio, matchMedia("(pointer: coarse)").matches ? 1.5 : 2));
|
|
525
|
+
this.fabricTexture.anisotropy = Math.min(4, this.renderer.capabilities.getMaxAnisotropy());
|
|
526
|
+
this.camera.position.z = 5;
|
|
527
|
+
this.scene.add(new THREE3.HemisphereLight(16777215, 7827851, 2.3));
|
|
528
|
+
const key = new THREE3.DirectionalLight(16777215, 2.2);
|
|
529
|
+
key.position.set(-3, 4, 5);
|
|
530
|
+
this.scene.add(key);
|
|
531
|
+
const back = new THREE3.DirectionalLight(16777215, 1.6);
|
|
532
|
+
back.position.set(3, 2, -4);
|
|
533
|
+
this.scene.add(back);
|
|
534
|
+
this.observer = new ResizeObserver(() => {
|
|
535
|
+
if (this.disposed || this.contextUnavailable)
|
|
536
|
+
return;
|
|
537
|
+
try {
|
|
538
|
+
const rect = canvas.getBoundingClientRect();
|
|
539
|
+
this.renderer.setSize(rect.width, rect.height, false);
|
|
540
|
+
this.render();
|
|
541
|
+
} catch {
|
|
542
|
+
this.fail();
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
this.observer.observe(canvas);
|
|
546
|
+
canvas.addEventListener("webglcontextlost", this.contextLost);
|
|
547
|
+
canvas.addEventListener("webglcontextrestored", this.contextRestored);
|
|
548
|
+
this.reducedMotion.addEventListener("change", this.resetCloth);
|
|
549
|
+
document.addEventListener("visibilitychange", this.resetCloth);
|
|
550
|
+
}
|
|
551
|
+
async load() {
|
|
552
|
+
const url = this.modelUrl;
|
|
553
|
+
const response = await fetch(url, { signal: this.loadController.signal });
|
|
554
|
+
if (!response.ok)
|
|
555
|
+
throw new Error(`Shirt model request failed (${response.status})`);
|
|
556
|
+
const data = await response.arrayBuffer();
|
|
557
|
+
this.loadController.signal.throwIfAborted();
|
|
558
|
+
const gltf = await new GLTFLoader().parseAsync(data, new URL(".", url).href);
|
|
559
|
+
if (this.disposed) {
|
|
560
|
+
this.disposeModel(gltf.scene);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
this.model = gltf.scene;
|
|
564
|
+
this.model.traverse((object) => {
|
|
565
|
+
if (!(object instanceof THREE3.Mesh))
|
|
566
|
+
return;
|
|
567
|
+
this.clothMeshes.push(new ShirtClothMesh2(object.geometry));
|
|
568
|
+
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
|
569
|
+
for (const material of materials) {
|
|
570
|
+
if (!(material instanceof THREE3.MeshStandardMaterial))
|
|
571
|
+
continue;
|
|
572
|
+
material.color.set(16777215);
|
|
573
|
+
material.side = THREE3.DoubleSide;
|
|
574
|
+
material.metalness = 0;
|
|
575
|
+
material.roughness = 1;
|
|
576
|
+
material.roughnessMap = this.fabricTexture;
|
|
577
|
+
material.bumpMap = this.fabricTexture;
|
|
578
|
+
material.bumpScale = 0.0012;
|
|
579
|
+
material.map = this.texture;
|
|
580
|
+
if (material instanceof THREE3.MeshPhysicalMaterial) {
|
|
581
|
+
material.sheen = 0.3;
|
|
582
|
+
material.sheenColorMap = null;
|
|
583
|
+
material.sheenRoughness = 1;
|
|
584
|
+
material.specularIntensity = 0.3;
|
|
585
|
+
material.clearcoat = 0;
|
|
586
|
+
this.fabricMaterials.push(material);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
this.scene.add(this.model);
|
|
591
|
+
this.paint();
|
|
592
|
+
await new Promise((resolve, reject) => {
|
|
593
|
+
this.firstFrame = { resolve, reject };
|
|
594
|
+
this.render();
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
update(slots, side, rotating, color = "#ffffff", showBidPrices = false, artwork = null, viewSurface) {
|
|
598
|
+
this.slots = slots;
|
|
599
|
+
if (side !== this.side || viewSurface !== this.viewSurface) {
|
|
600
|
+
this.viewSurface = viewSurface;
|
|
601
|
+
this.resetView();
|
|
602
|
+
}
|
|
603
|
+
this.side = side;
|
|
604
|
+
this.rotating = rotating;
|
|
605
|
+
this.shirtColor = color;
|
|
606
|
+
this.showBidPrices = showBidPrices;
|
|
607
|
+
this.artwork = artwork;
|
|
608
|
+
const urls = new Set([
|
|
609
|
+
...slots.map((slot) => slot.isReserved ? slot.reservedLogo : leader2(slot)?.logo),
|
|
610
|
+
artwork?.front?.url,
|
|
611
|
+
artwork?.back?.url
|
|
612
|
+
].filter((url) => !!url));
|
|
613
|
+
for (const [url, image] of this.images)
|
|
614
|
+
if (!urls.has(url)) {
|
|
615
|
+
image.onload = null;
|
|
616
|
+
this.images.delete(url);
|
|
617
|
+
}
|
|
618
|
+
for (const url of urls)
|
|
619
|
+
if (!this.images.has(url)) {
|
|
620
|
+
const image = new Image;
|
|
621
|
+
if (/^https?:\/\//i.test(url))
|
|
622
|
+
image.crossOrigin = "anonymous";
|
|
623
|
+
this.images.set(url, image);
|
|
624
|
+
image.onload = () => {
|
|
625
|
+
if (!this.disposed) {
|
|
626
|
+
this.paint();
|
|
627
|
+
this.render();
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
image.src = url;
|
|
631
|
+
}
|
|
632
|
+
this.paint();
|
|
633
|
+
this.render();
|
|
634
|
+
}
|
|
635
|
+
async snapshot(side = "front") {
|
|
636
|
+
return this.capture(side, "image/png");
|
|
637
|
+
}
|
|
638
|
+
async productionSnapshot(view) {
|
|
639
|
+
return this.capture(view, "image/webp");
|
|
640
|
+
}
|
|
641
|
+
async capture(view, mimeType) {
|
|
642
|
+
if (!this.model || this.disposed || this.contextUnavailable)
|
|
643
|
+
throw new Error("Shirt renderer unavailable");
|
|
644
|
+
await Promise.all([...this.images.values()].map((image) => image.decode()));
|
|
645
|
+
if (this.disposed || !this.model || this.contextUnavailable)
|
|
646
|
+
throw new Error("Shirt renderer unavailable");
|
|
647
|
+
this.resetCloth();
|
|
648
|
+
this.paintNow();
|
|
649
|
+
const rotation = this.model.rotation.clone();
|
|
650
|
+
const zoom = this.camera.zoom;
|
|
651
|
+
try {
|
|
652
|
+
this.model.rotation.set(view === "top" ? Math.PI / 2 : 0, view === "back" ? Math.PI : view === "left" ? Math.PI / 2 : view === "right" ? -Math.PI / 2 : 0, 0);
|
|
653
|
+
this.camera.zoom = 1;
|
|
654
|
+
this.camera.updateProjectionMatrix();
|
|
655
|
+
this.renderer.render(this.scene, this.camera);
|
|
656
|
+
const preview = document.createElement("canvas");
|
|
657
|
+
preview.width = 640;
|
|
658
|
+
preview.height = Math.round(640 * this.renderer.domElement.height / this.renderer.domElement.width);
|
|
659
|
+
const context = preview.getContext("2d");
|
|
660
|
+
if (!context)
|
|
661
|
+
throw new Error("Canvas unavailable");
|
|
662
|
+
if (mimeType === "image/webp") {
|
|
663
|
+
context.fillStyle = "#ffffff";
|
|
664
|
+
context.fillRect(0, 0, preview.width, preview.height);
|
|
665
|
+
}
|
|
666
|
+
context.drawImage(this.renderer.domElement, 0, 0, preview.width, preview.height);
|
|
667
|
+
return await new Promise((resolve, reject) => preview.toBlob((blob) => blob ? resolve(blob) : reject(new Error("Preview encoding failed")), mimeType, mimeType === "image/webp" ? 0.82 : undefined));
|
|
668
|
+
} finally {
|
|
669
|
+
this.model.rotation.copy(rotation);
|
|
670
|
+
this.camera.zoom = zoom;
|
|
671
|
+
this.camera.updateProjectionMatrix();
|
|
672
|
+
this.render();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
setSelectedSlot(id) {
|
|
676
|
+
if (id === this.selectedSlotId)
|
|
677
|
+
return;
|
|
678
|
+
this.selectedSlotId = id;
|
|
679
|
+
this.paint();
|
|
680
|
+
this.render();
|
|
681
|
+
}
|
|
682
|
+
setAppearance(options) {
|
|
683
|
+
if (options.palette)
|
|
684
|
+
this.palette = { ...defaultShirt3dPalette2, ...options.palette };
|
|
685
|
+
if (options.locale !== undefined)
|
|
686
|
+
this.priceFormatter = this.createPriceFormatter(options.locale);
|
|
687
|
+
this.paint();
|
|
688
|
+
this.render();
|
|
689
|
+
}
|
|
690
|
+
createPriceFormatter(locale) {
|
|
691
|
+
const options = { style: "currency", currency: "EUR", maximumFractionDigits: 0 };
|
|
692
|
+
try {
|
|
693
|
+
return new Intl.NumberFormat(locale || document.documentElement.lang || "es-ES", options);
|
|
694
|
+
} catch {
|
|
695
|
+
return new Intl.NumberFormat("es-ES", options);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
rotate(dx, dy) {
|
|
699
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy))
|
|
700
|
+
return;
|
|
701
|
+
const previousTilt = this.tilt;
|
|
702
|
+
this.rotation += dx * 0.01;
|
|
703
|
+
this.tilt = Math.max(-0.7, Math.min(0.7, this.tilt + dy * 0.008));
|
|
704
|
+
if (!this.reducedMotion.matches && !document.hidden && (dx || this.tilt !== previousTilt)) {
|
|
705
|
+
this.cloth.turn(dx * 0.01, this.tilt - previousTilt);
|
|
706
|
+
this.clothActive = true;
|
|
707
|
+
}
|
|
708
|
+
this.render();
|
|
709
|
+
}
|
|
710
|
+
zoomBy(delta) {
|
|
711
|
+
this.zoom = Math.max(0.7, Math.min(1.8, this.zoom + delta));
|
|
712
|
+
this.render();
|
|
713
|
+
}
|
|
714
|
+
resetView() {
|
|
715
|
+
this.rotation = this.viewSurface === "left-sleeve" ? Math.PI / 2 : this.viewSurface === "right-sleeve" ? -Math.PI / 2 : 0;
|
|
716
|
+
this.tilt = 0;
|
|
717
|
+
this.zoom = 1;
|
|
718
|
+
this.resetCloth();
|
|
719
|
+
}
|
|
720
|
+
surface(clientX, clientY) {
|
|
721
|
+
if (!this.model)
|
|
722
|
+
return;
|
|
723
|
+
const bounds = this.canvas.getBoundingClientRect();
|
|
724
|
+
this.raycaster.setFromCamera(new THREE3.Vector2((clientX - bounds.left) / bounds.width * 2 - 1, -((clientY - bounds.top) / bounds.height) * 2 + 1), this.camera);
|
|
725
|
+
const hit = this.raycaster.intersectObject(this.model, true).find((item) => item.uv);
|
|
726
|
+
if (!hit?.uv)
|
|
727
|
+
return;
|
|
728
|
+
const column = Math.min(3, Math.floor(hit.uv.x * 4));
|
|
729
|
+
let torso;
|
|
730
|
+
if (column >= 2 && hit.object instanceof THREE3.Mesh && hit.face) {
|
|
731
|
+
const geometry = hit.object.geometry;
|
|
732
|
+
const projection = geometry.getAttribute("wodiraArtworkUv");
|
|
733
|
+
const positions = geometry.getAttribute("position");
|
|
734
|
+
if (projection) {
|
|
735
|
+
const { a, b, c } = hit.face;
|
|
736
|
+
const weights = THREE3.Triangle.getBarycoord(hit.object.worldToLocal(hit.point.clone()), new THREE3.Vector3().fromBufferAttribute(positions, a), new THREE3.Vector3().fromBufferAttribute(positions, b), new THREE3.Vector3().fromBufferAttribute(positions, c), new THREE3.Vector3);
|
|
737
|
+
if (weights) {
|
|
738
|
+
const uv = new THREE3.Vector3().addScaledVector(new THREE3.Vector3().fromBufferAttribute(projection, a), weights.x).addScaledVector(new THREE3.Vector3().fromBufferAttribute(projection, b), weights.y).addScaledVector(new THREE3.Vector3().fromBufferAttribute(projection, c), weights.z);
|
|
739
|
+
torso = {
|
|
740
|
+
side: uv.z >= 0 ? "front" : "back",
|
|
741
|
+
surface: undefined,
|
|
742
|
+
x: (uv.z >= 0 ? uv.x : 1 - uv.x) * 100,
|
|
743
|
+
y: uv.y * 100
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
torso,
|
|
750
|
+
side: column === 1 ? "back" : "front",
|
|
751
|
+
surface: column === 2 ? "left-sleeve" : column === 3 ? "right-sleeve" : undefined,
|
|
752
|
+
x: (hit.uv.x * 4 - column) * 100,
|
|
753
|
+
y: hit.uv.y * 100
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
pick(clientX, clientY) {
|
|
757
|
+
const point = this.surface(clientX, clientY);
|
|
758
|
+
if (!point)
|
|
759
|
+
return;
|
|
760
|
+
for (const candidate of [point, point.torso]) {
|
|
761
|
+
if (!candidate)
|
|
762
|
+
continue;
|
|
763
|
+
const slot = this.slots.find((slot) => candidate.side === slot.side && candidate.surface === slot.surface && candidate.x >= slot.x && candidate.x <= slot.x + slot.width && candidate.y >= slot.y && candidate.y <= slot.y + slot.height);
|
|
764
|
+
if (slot)
|
|
765
|
+
return slot;
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
paint() {
|
|
770
|
+
this.textureDirty = true;
|
|
771
|
+
}
|
|
772
|
+
paintNow() {
|
|
773
|
+
const ctx = this.atlas.getContext("2d");
|
|
774
|
+
if (!ctx)
|
|
775
|
+
return;
|
|
776
|
+
ctx.setTransform(this.atlas.width / 4000, 0, 0, this.atlas.height / 1120, 0, 0);
|
|
777
|
+
const dye = new THREE3.Color(this.shirtColor).lerp(new THREE3.Color(16777215), 0.004);
|
|
778
|
+
if (this.model && !this.artworkProjection) {
|
|
779
|
+
this.resetCloth();
|
|
780
|
+
this.artworkProjection = new ShirtArtworkProjection2(this.model);
|
|
781
|
+
}
|
|
782
|
+
this.artworkProjection?.update(this.artwork, this.images, dye);
|
|
783
|
+
for (const material of this.fabricMaterials) {
|
|
784
|
+
material.sheenColor.copy(dye).lerp(new THREE3.Color(16777215), 0.35);
|
|
785
|
+
}
|
|
786
|
+
ctx.clearRect(0, 0, 4000, 1120);
|
|
787
|
+
for (const slot of this.slots) {
|
|
788
|
+
const x = (slot.surface === "left-sleeve" ? 2000 : slot.surface === "right-sleeve" ? 3000 : slot.side === "back" ? 1000 : 0) + slot.x * 10, y = slot.y * 11.2, w = slot.width * 10, h = slot.height * 11.2;
|
|
789
|
+
ctx.save();
|
|
790
|
+
if (slot.surface === "left-sleeve") {
|
|
791
|
+
ctx.translate(2 * x + w, 0);
|
|
792
|
+
ctx.scale(-1, 1);
|
|
793
|
+
}
|
|
794
|
+
const currentBid = leader2(slot);
|
|
795
|
+
const logo = slot.isReserved ? slot.reservedLogo : currentBid?.logo;
|
|
796
|
+
const image = logo ? this.images.get(logo) : undefined;
|
|
797
|
+
if (image?.complete && image.naturalWidth) {
|
|
798
|
+
const scale = Math.min(w / image.naturalWidth, h / image.naturalHeight);
|
|
799
|
+
const iw = image.naturalWidth * scale, ih = image.naturalHeight * scale;
|
|
800
|
+
ctx.drawImage(image, x + (w - iw) / 2, y + (h - ih) / 2, iw, ih);
|
|
801
|
+
} else if (this.rotating) {
|
|
802
|
+
ctx.fillStyle = slot.isReserved ? this.palette.reserved : slot.id === "drawing-preview" ? this.palette.slotPreview : this.palette.slot;
|
|
803
|
+
ctx.fillRect(x, y, w, h);
|
|
804
|
+
ctx.strokeStyle = this.palette.slotText;
|
|
805
|
+
ctx.lineWidth = 5;
|
|
806
|
+
ctx.strokeRect(x, y, w, h);
|
|
807
|
+
ctx.fillStyle = this.palette.slotText;
|
|
808
|
+
ctx.font = "bold 24px sans-serif";
|
|
809
|
+
ctx.textBaseline = "middle";
|
|
810
|
+
ctx.textAlign = "center";
|
|
811
|
+
ctx.fillText(slot.name, x + w / 2, y + h * (this.showBidPrices ? 0.36 : 0.5), w - 10);
|
|
812
|
+
}
|
|
813
|
+
if (slot.id === this.selectedSlotId) {
|
|
814
|
+
ctx.strokeStyle = this.palette.selected;
|
|
815
|
+
ctx.lineWidth = 12;
|
|
816
|
+
ctx.strokeRect(x - 6, y - 6, w + 12, h + 12);
|
|
817
|
+
}
|
|
818
|
+
if (this.showBidPrices && !slot.isReserved && slot.id !== "drawing-preview") {
|
|
819
|
+
this.paintBidPrice(ctx, x, y, w, h, slot.bidPrice ?? currentBid?.amount ?? slot.startingPrice, !!currentBid);
|
|
820
|
+
}
|
|
821
|
+
ctx.restore();
|
|
822
|
+
}
|
|
823
|
+
this.texture.needsUpdate = true;
|
|
824
|
+
}
|
|
825
|
+
paintBidPrice(ctx, x, y, width, height, amount, hasBid) {
|
|
826
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
827
|
+
return;
|
|
828
|
+
const label = this.priceFormatter.format(amount);
|
|
829
|
+
const badgeHeight = Math.min(56, height * 0.43);
|
|
830
|
+
const fontSize = Math.min(46, badgeHeight * 0.82);
|
|
831
|
+
ctx.save();
|
|
832
|
+
ctx.font = `700 ${fontSize}px sans-serif`;
|
|
833
|
+
const badgeWidth = Math.min(width - 12, ctx.measureText(label).width + 26);
|
|
834
|
+
const badgeX = x + (width - badgeWidth) / 2;
|
|
835
|
+
const badgeY = y + height - badgeHeight - 8;
|
|
836
|
+
ctx.beginPath();
|
|
837
|
+
ctx.roundRect(badgeX, badgeY, badgeWidth, badgeHeight, Math.min(15, badgeHeight / 3));
|
|
838
|
+
ctx.fillStyle = hasBid ? this.palette.priceLeading : this.palette.price;
|
|
839
|
+
ctx.shadowColor = "rgba(16, 22, 50, 0.3)";
|
|
840
|
+
ctx.shadowBlur = 10;
|
|
841
|
+
ctx.shadowOffsetY = 3;
|
|
842
|
+
ctx.fill();
|
|
843
|
+
ctx.shadowColor = "transparent";
|
|
844
|
+
ctx.strokeStyle = this.palette.slotText;
|
|
845
|
+
ctx.lineWidth = 3;
|
|
846
|
+
ctx.stroke();
|
|
847
|
+
ctx.fillStyle = this.palette.slotText;
|
|
848
|
+
ctx.textAlign = "center";
|
|
849
|
+
ctx.textBaseline = "middle";
|
|
850
|
+
ctx.fillText(label, x + width / 2, badgeY + badgeHeight / 2, badgeWidth - 14);
|
|
851
|
+
ctx.restore();
|
|
852
|
+
}
|
|
853
|
+
render() {
|
|
854
|
+
if (this.disposed || this.contextUnavailable)
|
|
855
|
+
return;
|
|
856
|
+
if (this.frame)
|
|
857
|
+
return;
|
|
858
|
+
if (!this.lastFrameTime)
|
|
859
|
+
this.lastFrameTime = performance.now();
|
|
860
|
+
this.frame = requestAnimationFrame((time) => {
|
|
861
|
+
this.frame = 0;
|
|
862
|
+
if (this.disposed || this.contextUnavailable)
|
|
863
|
+
return;
|
|
864
|
+
try {
|
|
865
|
+
const elapsed = Math.max(0, (time - this.lastFrameTime) / 1000);
|
|
866
|
+
this.lastFrameTime = time;
|
|
867
|
+
if (this.clothActive) {
|
|
868
|
+
this.clothActive = this.cloth.advance(elapsed);
|
|
869
|
+
for (const mesh of this.clothMeshes) {
|
|
870
|
+
if (this.clothActive)
|
|
871
|
+
mesh.apply(this.cloth);
|
|
872
|
+
else
|
|
873
|
+
mesh.restore();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (this.textureDirty) {
|
|
877
|
+
this.paintNow();
|
|
878
|
+
this.textureDirty = false;
|
|
879
|
+
}
|
|
880
|
+
if (this.model)
|
|
881
|
+
this.model.rotation.set(this.tilt, (this.side === "back" ? Math.PI : 0) + this.rotation, 0);
|
|
882
|
+
this.camera.zoom = this.zoom;
|
|
883
|
+
this.camera.updateProjectionMatrix();
|
|
884
|
+
this.renderer.render(this.scene, this.camera);
|
|
885
|
+
if (this.renderer.getContext().isContextLost())
|
|
886
|
+
return;
|
|
887
|
+
if (this.model && this.canvas.clientWidth > 0 && this.canvas.clientHeight > 0) {
|
|
888
|
+
this.firstFrame?.resolve();
|
|
889
|
+
this.firstFrame = undefined;
|
|
890
|
+
clearTimeout(this.recoveryTimer);
|
|
891
|
+
this.recovering(false);
|
|
892
|
+
}
|
|
893
|
+
if (this.clothActive)
|
|
894
|
+
this.render();
|
|
895
|
+
else
|
|
896
|
+
this.lastFrameTime = 0;
|
|
897
|
+
} catch {
|
|
898
|
+
this.fail();
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
resetCloth = () => {
|
|
903
|
+
this.cloth.reset();
|
|
904
|
+
this.clothActive = false;
|
|
905
|
+
this.lastFrameTime = this.frame ? performance.now() : 0;
|
|
906
|
+
for (const mesh of this.clothMeshes)
|
|
907
|
+
mesh.restore();
|
|
908
|
+
this.render();
|
|
909
|
+
};
|
|
910
|
+
createFabricTexture() {
|
|
911
|
+
const size = 128;
|
|
912
|
+
const data = new Uint8Array(size * size * 4);
|
|
913
|
+
for (let y = 0;y < size; y++)
|
|
914
|
+
for (let x = 0;x < size; x++) {
|
|
915
|
+
const row = y / 16 * Math.PI * 2;
|
|
916
|
+
const column = x / 16 * Math.PI * 2;
|
|
917
|
+
const yarn = Math.sin(column + Math.sin(row) * 0.65) * Math.cos(row);
|
|
918
|
+
const fibre = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
|
|
919
|
+
const grain = (fibre - Math.floor(fibre) - 0.5) * 10;
|
|
920
|
+
const offset = (y * size + x) * 4;
|
|
921
|
+
data[offset] = Math.round(128 + yarn * 35 + grain);
|
|
922
|
+
data[offset + 1] = Math.round(237 + yarn * 10 + grain * 0.3);
|
|
923
|
+
data[offset + 2] = 0;
|
|
924
|
+
data[offset + 3] = 255;
|
|
925
|
+
}
|
|
926
|
+
const texture = new THREE3.DataTexture(data, size, size, THREE3.RGBAFormat);
|
|
927
|
+
texture.wrapS = texture.wrapT = THREE3.RepeatWrapping;
|
|
928
|
+
texture.repeat.set(64, 20);
|
|
929
|
+
texture.generateMipmaps = true;
|
|
930
|
+
texture.minFilter = THREE3.LinearMipmapLinearFilter;
|
|
931
|
+
texture.magFilter = THREE3.LinearFilter;
|
|
932
|
+
texture.needsUpdate = true;
|
|
933
|
+
return texture;
|
|
934
|
+
}
|
|
935
|
+
contextLost = (event) => {
|
|
936
|
+
event.preventDefault();
|
|
937
|
+
if (this.disposed)
|
|
938
|
+
return;
|
|
939
|
+
this.contextUnavailable = true;
|
|
940
|
+
cancelAnimationFrame(this.frame);
|
|
941
|
+
this.frame = 0;
|
|
942
|
+
this.recovering(true);
|
|
943
|
+
clearTimeout(this.recoveryTimer);
|
|
944
|
+
this.recoveryTimer = setTimeout(() => this.fail(), 8000);
|
|
945
|
+
};
|
|
946
|
+
contextRestored = () => {
|
|
947
|
+
if (this.disposed)
|
|
948
|
+
return;
|
|
949
|
+
this.contextUnavailable = false;
|
|
950
|
+
try {
|
|
951
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
952
|
+
this.renderer.setSize(rect.width, rect.height, false);
|
|
953
|
+
this.paint();
|
|
954
|
+
this.resetCloth();
|
|
955
|
+
} catch {
|
|
956
|
+
this.fail();
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
disposeModel(model) {
|
|
960
|
+
model.traverse((object) => {
|
|
961
|
+
if (!(object instanceof THREE3.Mesh))
|
|
962
|
+
return;
|
|
963
|
+
object.geometry.dispose();
|
|
964
|
+
for (const material of Array.isArray(object.material) ? object.material : [object.material])
|
|
965
|
+
material.dispose();
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
dispose() {
|
|
969
|
+
if (this.disposed)
|
|
970
|
+
return;
|
|
971
|
+
this.disposed = true;
|
|
972
|
+
this.loadController.abort();
|
|
973
|
+
clearTimeout(this.recoveryTimer);
|
|
974
|
+
this.firstFrame?.reject(new Error("Shirt renderer disposed"));
|
|
975
|
+
this.firstFrame = undefined;
|
|
976
|
+
cancelAnimationFrame(this.frame);
|
|
977
|
+
this.observer.disconnect();
|
|
978
|
+
this.canvas.removeEventListener("webglcontextlost", this.contextLost);
|
|
979
|
+
this.canvas.removeEventListener("webglcontextrestored", this.contextRestored);
|
|
980
|
+
this.reducedMotion.removeEventListener("change", this.resetCloth);
|
|
981
|
+
document.removeEventListener("visibilitychange", this.resetCloth);
|
|
982
|
+
this.clothMeshes = [];
|
|
983
|
+
for (const image of this.images.values())
|
|
984
|
+
image.onload = null;
|
|
985
|
+
this.images.clear();
|
|
986
|
+
this.texture.dispose();
|
|
987
|
+
this.artworkProjection?.dispose();
|
|
988
|
+
this.fabricTexture.dispose();
|
|
989
|
+
this.fabricMaterials = [];
|
|
990
|
+
if (this.model)
|
|
991
|
+
this.disposeModel(this.model);
|
|
992
|
+
this.renderer.dispose();
|
|
993
|
+
this.renderer.forceContextLoss();
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
export { leader2, minimumBid2, hasPaidBids2, reserveBid2, settleBid2, demoCampaign2, ShirtClothMotion2, ShirtClothMesh2, shirtArmhole2, mapShirtHeight2, paintShirtArtwork2, ShirtArtworkProjection2, defaultShirt3dPalette2, SHIRT_MODEL_PATH2, Shirt3dRenderer2 };
|