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