Skip to content

Commit 30ee60d

Browse files
authored
Merge pull request #29 from hackclub/staging
Add swag modal with stickers and 3D keychain preview and expand FAQ and update eligibility rules
2 parents 399cd1e + 65d4ec3 commit 30ee60d

File tree

5 files changed

+265
-2
lines changed

5 files changed

+265
-2
lines changed

src/lib/assets/keyring_colored.3mf

92.3 KB
Binary file not shown.

src/lib/assets/sticker1.png

74.7 KB
Loading

src/lib/assets/sticker2.png

111 KB
Loading

src/routes/+page.svelte

Lines changed: 264 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
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_colored.3mf?url';
13+
import sticker1Image from '$lib/assets/sticker1.png';
14+
import sticker2Image from '$lib/assets/sticker2.png';
1215
1316
let { data } = $props();
1417
@@ -18,11 +21,12 @@
1821
import { onMount } from 'svelte';
1922
import Head from '$lib/components/Head.svelte';
2023
21-
// Necessary for camera/plane rotation
2224
let degree = Math.PI / 180;
25+
let showStickersSection = $state(false);
26+
let keyringInitialized = false;
2327
24-
// Create scene
2528
const scene = new THREE.Scene();
29+
const keyringScene = new THREE.Scene();
2630
2731
onMount(() => {
2832
if (!model) {
@@ -210,10 +214,268 @@
210214
};
211215
animate();
212216
});
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+
});
213379
</script>
214380

215381
<Head title="" />
216382

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+
217479
<OrpheusFlag />
218480

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

src/routes/Rules.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<h3 class="mb-1 text-lg font-semibold">Eligibility</h3>
1010
<ul class="list-inside list-disc text-sm">
1111
<li>You must be 13-18 years old.</li>
12+
<li>You cannot be banned from HackaTime.</li>
1213
<li>
1314
You must be part of the <a
1415
href="https://hackclub.com/slack/"

0 commit comments

Comments
 (0)