Skip to content

Commit 7d03d0c

Browse files
committed
Add swag modal with stickers and 3D keychain preview
1 parent 6f3ce9b commit 7d03d0c

File tree

3 files changed

+258
-2
lines changed

3 files changed

+258
-2
lines changed

src/lib/assets/sticker1.png

74.7 KB
Loading

src/lib/assets/sticker2.png

111 KB
Loading

src/routes/+page.svelte

Lines changed: 258 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,25 @@
99
1010
import model from '$lib/assets/Construct Logo.3mf?url';
1111
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';
1215
1316
let { data } = $props();
1417
1518
import * as THREE from 'three';
1619
import { ThreeMFLoader } from 'three/examples/jsm/Addons.js';
20+
import { OBJLoader } from 'three/examples/jsm/Addons.js';
1721
import { OrbitControls } from 'three/examples/jsm/Addons.js';
1822
import { onMount } from 'svelte';
1923
import Head from '$lib/components/Head.svelte';
2024
21-
// Necessary for camera/plane rotation
2225
let degree = Math.PI / 180;
26+
let showStickersSection = $state(false);
27+
let keyringInitialized = false;
2328
24-
// Create scene
2529
const scene = new THREE.Scene();
30+
const keyringScene = new THREE.Scene();
2631
2732
onMount(() => {
2833
if (!model) {
@@ -210,10 +215,261 @@
210215
};
211216
animate();
212217
});
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+
});
213376
</script>
214377

215378
<Head title="" />
216379

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+
217473
<OrpheusFlag />
218474

219475
<div class="flex w-full flex-col items-center justify-center px-10 lg:flex-row">

0 commit comments

Comments
 (0)