|
9 | 9 |
|
10 | 10 | import model from '$lib/assets/Construct Logo.3mf?url'; |
11 | 11 | import modelImage from '$lib/assets/model.png'; |
| 12 | + import keyringModel from '$lib/assets/keyring_colored.3mf?url'; |
| 13 | + import sticker1Image from '$lib/assets/sticker1.png'; |
| 14 | + import sticker2Image from '$lib/assets/sticker2.png'; |
12 | 15 |
|
13 | 16 | let { data } = $props(); |
14 | 17 |
|
|
18 | 21 | import { onMount } from 'svelte'; |
19 | 22 | import Head from '$lib/components/Head.svelte'; |
20 | 23 |
|
21 | | - // Necessary for camera/plane rotation |
22 | 24 | let degree = Math.PI / 180; |
| 25 | + let showStickersSection = $state(false); |
| 26 | + let keyringInitialized = false; |
23 | 27 |
|
24 | | - // Create scene |
25 | 28 | const scene = new THREE.Scene(); |
| 29 | + const keyringScene = new THREE.Scene(); |
26 | 30 |
|
27 | 31 | onMount(() => { |
28 | 32 | if (!model) { |
|
210 | 214 | }; |
211 | 215 | animate(); |
212 | 216 | }); |
| 217 | +
|
| 218 | + $effect(() => { |
| 219 | + if (!showStickersSection) { |
| 220 | + return; |
| 221 | + } |
| 222 | +
|
| 223 | + if (keyringInitialized || !keyringModel) { |
| 224 | + return; |
| 225 | + } |
| 226 | +
|
| 227 | + setTimeout(() => { |
| 228 | + let keyringCanvas = document.querySelector(`#keyring-canvas`); |
| 229 | +
|
| 230 | + if (!keyringCanvas) { |
| 231 | + return; |
| 232 | + } |
| 233 | +
|
| 234 | + keyringInitialized = true; |
| 235 | +
|
| 236 | + const keyringRenderer = new THREE.WebGLRenderer({ |
| 237 | + canvas: keyringCanvas, |
| 238 | + antialias: true, |
| 239 | + alpha: true |
| 240 | + }); |
| 241 | +
|
| 242 | + keyringRenderer.setClearColor(0xffffff, 0); |
| 243 | + keyringRenderer.setPixelRatio(window.devicePixelRatio); |
| 244 | + keyringRenderer.shadowMap.enabled = true; |
| 245 | + keyringRenderer.shadowMap.type = THREE.PCFSoftShadowMap; |
| 246 | +
|
| 247 | + const keyringCamera = new THREE.PerspectiveCamera(40, 2, 1, 1000); |
| 248 | + keyringCamera.rotation.x = -45 * degree; |
| 249 | +
|
| 250 | + let keyringControls = new OrbitControls(keyringCamera, keyringRenderer.domElement); |
| 251 | + keyringControls.target.set(0, 0, 0); |
| 252 | + keyringControls.rotateSpeed = 0.6; |
| 253 | + keyringControls.enablePan = false; |
| 254 | + keyringControls.dampingFactor = 0.1; |
| 255 | + keyringControls.enableDamping = true; |
| 256 | + keyringControls.autoRotate = true; |
| 257 | + keyringControls.autoRotateSpeed = 3; |
| 258 | + keyringControls.update(); |
| 259 | +
|
| 260 | + const keyringHemisphere = new THREE.HemisphereLight(0xffffff, 0xffffff, 4); |
| 261 | + keyringScene.add(keyringHemisphere); |
| 262 | +
|
| 263 | + const keyringDirectional = new THREE.DirectionalLight(0xffffff, 1); |
| 264 | + keyringDirectional.castShadow = true; |
| 265 | + keyringDirectional.shadow.mapSize.width = 2048; |
| 266 | + keyringDirectional.shadow.mapSize.height = 2048; |
| 267 | + keyringScene.add(keyringDirectional); |
| 268 | +
|
| 269 | + const keyringDirectional2 = new THREE.DirectionalLight(0xffffff, 1); |
| 270 | + keyringDirectional2.castShadow = true; |
| 271 | + keyringDirectional2.shadow.mapSize.width = 2048; |
| 272 | + keyringDirectional2.shadow.mapSize.height = 2048; |
| 273 | + keyringScene.add(keyringDirectional2); |
| 274 | +
|
| 275 | + function resizeKeyringCanvasToDisplaySize() { |
| 276 | + const canvas = keyringRenderer.domElement; |
| 277 | + const width = canvas.clientWidth; |
| 278 | + const height = canvas.clientHeight; |
| 279 | + if (canvas.width !== width || canvas.height !== height) { |
| 280 | + keyringRenderer.setSize(width, height, false); |
| 281 | + keyringRenderer.setPixelRatio(window.devicePixelRatio); |
| 282 | + keyringCamera.aspect = width / height; |
| 283 | + keyringCamera.updateProjectionMatrix(); |
| 284 | + } |
| 285 | + } |
| 286 | +
|
| 287 | + function parseKeyringObject(object: THREE.Group<THREE.Object3DEventMap>) { |
| 288 | + object = object as THREE.Group<THREE.Object3DEventMap> & { children: THREE.Mesh[] }; |
| 289 | +
|
| 290 | + object.rotation.x = THREE.MathUtils.degToRad(-90); |
| 291 | +
|
| 292 | + const aabb = new THREE.Box3().setFromObject(object); |
| 293 | + const center = aabb.getCenter(new THREE.Vector3()); |
| 294 | +
|
| 295 | + object.position.x += object.position.x - center.x; |
| 296 | + object.position.y += object.position.y - center.y; |
| 297 | + object.position.z += object.position.z - center.z; |
| 298 | +
|
| 299 | + keyringControls.reset(); |
| 300 | +
|
| 301 | + var box = new THREE.Box3().setFromObject(object); |
| 302 | + const size = new THREE.Vector3(); |
| 303 | + box.getSize(size); |
| 304 | + const largestDimension = Math.max(size.x, size.y, size.z); |
| 305 | +
|
| 306 | + keyringCamera.position.z = largestDimension * 0.3; |
| 307 | + keyringCamera.position.y = largestDimension * 1.38; |
| 308 | +
|
| 309 | + keyringDirectional.position.set( |
| 310 | + largestDimension * 2, |
| 311 | + largestDimension * 2, |
| 312 | + largestDimension * 2 |
| 313 | + ); |
| 314 | + keyringDirectional2.position.set( |
| 315 | + -largestDimension * 2, |
| 316 | + largestDimension * 2, |
| 317 | + -largestDimension * 2 |
| 318 | + ); |
| 319 | +
|
| 320 | + keyringCamera.near = largestDimension * 0.001; |
| 321 | + keyringCamera.far = largestDimension * 10; |
| 322 | + keyringCamera.updateProjectionMatrix(); |
| 323 | +
|
| 324 | + const edgeLines: { lines: THREE.LineSegments; mesh: THREE.Mesh }[] = []; |
| 325 | +
|
| 326 | + object.traverse(function (child) { |
| 327 | + child.castShadow = true; |
| 328 | + child.receiveShadow = true; |
| 329 | +
|
| 330 | + const mesh = child as THREE.Mesh; |
| 331 | +
|
| 332 | + const edges = new THREE.EdgesGeometry(mesh.geometry); |
| 333 | + const lines = new THREE.LineSegments( |
| 334 | + edges, |
| 335 | + new THREE.LineBasicMaterial({ |
| 336 | + color: 0xf3dcc6, |
| 337 | + linewidth: 1, |
| 338 | + polygonOffset: true, |
| 339 | + polygonOffsetFactor: -1, |
| 340 | + polygonOffsetUnits: -1 |
| 341 | + }) |
| 342 | + ); |
| 343 | +
|
| 344 | + lines.position.copy(mesh.position); |
| 345 | + lines.rotation.copy(mesh.rotation); |
| 346 | +
|
| 347 | + edgeLines.push({ lines, mesh }); |
| 348 | + }); |
| 349 | +
|
| 350 | + edgeLines.forEach(({ lines, mesh }) => { |
| 351 | + mesh.add(lines); |
| 352 | + }); |
| 353 | +
|
| 354 | + keyringScene.add(object); |
| 355 | + } |
| 356 | +
|
| 357 | + var threemfLoader = new ThreeMFLoader(); |
| 358 | +
|
| 359 | + threemfLoader.load( |
| 360 | + keyringModel, |
| 361 | + parseKeyringObject, |
| 362 | + (xhr) => { |
| 363 | + console.log('Keyring: ' + (xhr.loaded / xhr.total) * 100 + '% loaded'); |
| 364 | + }, |
| 365 | + (error) => { |
| 366 | + console.error('Keyring error:', error); |
| 367 | + } |
| 368 | + ); |
| 369 | +
|
| 370 | + const animateKeyring = function () { |
| 371 | + requestAnimationFrame(animateKeyring); |
| 372 | + keyringControls.update(); |
| 373 | + keyringRenderer.render(keyringScene, keyringCamera); |
| 374 | + resizeKeyringCanvasToDisplaySize(); |
| 375 | + }; |
| 376 | + animateKeyring(); |
| 377 | + }, 100); |
| 378 | + }); |
213 | 379 | </script> |
214 | 380 |
|
215 | 381 | <Head title="" /> |
216 | 382 |
|
| 383 | +{#if !showStickersSection} |
| 384 | + <button |
| 385 | + class="button md fixed top-4 right-4 z-50 border-3 border-orange-900 bg-orange-800 outline-orange-50 transition-all hover:scale-105 hover:bg-orange-700 animate-[bounce_2.5s_ease-in-out_infinite]" |
| 386 | + style="transform: rotate(-2deg);" |
| 387 | + onclick={() => { |
| 388 | + keyringInitialized = false; |
| 389 | + showStickersSection = !showStickersSection; |
| 390 | + }} |
| 391 | + > |
| 392 | + Free swag! |
| 393 | + </button> |
| 394 | +{/if} |
| 395 | + |
| 396 | +{#if showStickersSection} |
| 397 | + <div |
| 398 | + class="fixed inset-0 z-40 flex items-center justify-center bg-black bg-opacity-70 p-4" |
| 399 | + role="dialog" |
| 400 | + aria-modal="true" |
| 401 | + tabindex="0" |
| 402 | + onclick={(e) => { |
| 403 | + if (e.target === e.currentTarget) { |
| 404 | + showStickersSection = false; |
| 405 | + } |
| 406 | + }} |
| 407 | + onkeydown={(e) => e.key === 'Escape' && (showStickersSection = false)} |
| 408 | + > |
| 409 | + <div |
| 410 | + class="relative max-h-[95vh] w-full max-w-5xl overflow-y-auto rounded-lg border-3 border-primary-900 bg-primary-950 p-8 shadow-2xl" |
| 411 | + role="document" |
| 412 | + tabindex="-1" |
| 413 | + > |
| 414 | + <button |
| 415 | + class="button sm absolute top-4 right-4 z-10 border-2 border-primary-900 bg-primary-800 outline-primary-50 hover:bg-primary-700" |
| 416 | + onclick={() => (showStickersSection = false)} |
| 417 | + aria-label="Close dialog" |
| 418 | + > |
| 419 | + Close |
| 420 | + </button> |
| 421 | + |
| 422 | + <div class="mx-auto max-w-4xl"> |
| 423 | + <div class="mb-8 text-center"> |
| 424 | + <h2 class="mb-2 text-2xl font-bold sm:text-3xl"> |
| 425 | + Free swag with your first submission |
| 426 | + </h2> |
| 427 | + <p class="text-lg font-medium text-primary-300"> |
| 428 | + Ship a project, get exclusive Construct goodies |
| 429 | + </p> |
| 430 | + </div> |
| 431 | + |
| 432 | + <div class="grid gap-6 sm:grid-cols-2"> |
| 433 | + <div class="themed-box p-6"> |
| 434 | + <div class="mb-4 flex h-56 items-center justify-center gap-3 overflow-hidden rounded-lg border-2 border-primary-900 bg-primary-900"> |
| 435 | + <img |
| 436 | + src={sticker1Image} |
| 437 | + alt="Construct sticker 1" |
| 438 | + class="h-40 w-40 animate-[spin_20s_linear_infinite] object-contain" |
| 439 | + style="animation-direction: normal;" |
| 440 | + /> |
| 441 | + <img |
| 442 | + src={sticker2Image} |
| 443 | + alt="Construct sticker 2" |
| 444 | + class="h-40 w-40 animate-[spin_20s_linear_infinite] object-contain" |
| 445 | + style="animation-direction: reverse;" |
| 446 | + /> |
| 447 | + </div> |
| 448 | + <div class="text-center"> |
| 449 | + <h3 class="mb-2 text-xl font-bold">Sticker Pack</h3> |
| 450 | + <p class="text-sm text-primary-300"> |
| 451 | + Sticker 1 and Sticker 2—both included |
| 452 | + </p> |
| 453 | + </div> |
| 454 | + </div> |
| 455 | + |
| 456 | + <div class="themed-box p-6"> |
| 457 | + <div class="mb-4 flex h-56 items-center justify-center overflow-hidden rounded-lg border-2 border-primary-900 bg-primary-900"> |
| 458 | + <canvas class="h-full w-full" width="200" height="200" id="keyring-canvas"></canvas> |
| 459 | + </div> |
| 460 | + <div class="text-center"> |
| 461 | + <h3 class="mb-2 text-xl font-bold">3D Keychain</h3> |
| 462 | + <p class="text-sm text-primary-300"> |
| 463 | + Custom 3D printed keychain |
| 464 | + </p> |
| 465 | + </div> |
| 466 | + </div> |
| 467 | + </div> |
| 468 | + |
| 469 | + <div class="themed-box mt-6 p-6 text-center"> |
| 470 | + <p class="font-medium"> |
| 471 | + <strong>How it works:</strong> Submit your first CAD project and we'll mail these to you—completely free! |
| 472 | + </p> |
| 473 | + </div> |
| 474 | + </div> |
| 475 | + </div> |
| 476 | + </div> |
| 477 | +{/if} |
| 478 | + |
217 | 479 | <OrpheusFlag /> |
218 | 480 |
|
219 | 481 | <div class="flex w-full flex-col items-center justify-center px-10 lg:flex-row"> |
|
0 commit comments