Initial import
103
configs/index.json
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
{
|
||||
"brand": "LASER EVERYTHING",
|
||||
"grid": { "gap": 12, "fit": "stretch" },
|
||||
"reveal": { "durationMs": 2600, "revertDelayMs": 700 },
|
||||
|
||||
"hint": {
|
||||
"enabled": true,
|
||||
"rows": 24,
|
||||
"textCase": "upper",
|
||||
"fontPx": 32,
|
||||
"opacity": 0.02,
|
||||
"angleDeg": -12,
|
||||
"rowGapPx": 8,
|
||||
"spacing": "\u00A0",
|
||||
"planeScale": 3,
|
||||
"offsetAmpPct": 35,
|
||||
"featherPct": 0
|
||||
},
|
||||
|
||||
"tiles": [
|
||||
{
|
||||
"title": "Training",
|
||||
"href": "https://lasereverything.net/training",
|
||||
"icon": "/icons/training.png",
|
||||
"color": "#2563eb",
|
||||
"size": 1,
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "Get one on one training from our expert staff so you can end the cycle of frustration. Start having fun and MAKING with your equipment."
|
||||
},
|
||||
{
|
||||
"title": "Ethics",
|
||||
"href": "https://lasereverything.net/ethics",
|
||||
"icon": "/icons/ethics.png",
|
||||
"color": "#d60202",
|
||||
"size": 1,
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "Our standards for safe, fair, and responsible making."
|
||||
},
|
||||
{
|
||||
"title": "Podcasting",
|
||||
"href": "https://podcast.makearmy.io",
|
||||
"icon": "/icons/castopod.png",
|
||||
"image": "",
|
||||
"color": "#7e02d6",
|
||||
"size": "1x2",
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "Long-form chats on lasers, materials, and maker culture."
|
||||
},
|
||||
{
|
||||
"title": "Buying Guide",
|
||||
"href": "https://makearmy.io/buying-guide",
|
||||
"icon": "/icons/bg.png",
|
||||
"color": "#145221",
|
||||
"size": 1,
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "Curated picks with real-world testing and notes."
|
||||
},
|
||||
{
|
||||
"title": "Starter Settings Packs",
|
||||
"href": "https://masters.lasereverything.net",
|
||||
"icon": "/icons/lma.png",
|
||||
"color": "#0600bd",
|
||||
"size": "2x1",
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "Dial-in faster with tested baseline settings. 80% of the work is done for you, loosely preconverted for every wattage and lens combination."
|
||||
},
|
||||
{
|
||||
"title": "MakeArmy",
|
||||
"href": "https://makearmy.io",
|
||||
"icon": "/icons/makearmy.png",
|
||||
"color": "#bd006b",
|
||||
"size": "2x1",
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "Community tools, services, and the latest projects."
|
||||
},
|
||||
{
|
||||
"title": "Support Us",
|
||||
"href": "https://lasereverything.net/support",
|
||||
"icon": "/icons/support.png",
|
||||
"color": "#02bad6",
|
||||
"size": "2x1",
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "Help us keep guides and tools free for the community."
|
||||
},
|
||||
{
|
||||
"title": "Social Media",
|
||||
"href": "https://lasereverything.net/social",
|
||||
"icon": "/icons/youtube.png",
|
||||
"color": "#00a603",
|
||||
"size": "1x1",
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "Find us on all of the hippest global social media conglomerates!"
|
||||
}
|
||||
]
|
||||
}
|
||||
83
configs/social.json
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"brand": "LASER EVERYTHING",
|
||||
"grid": { "gap": 12, "fit": "stretch" },
|
||||
"reveal": { "durationMs": 2600, "revertDelayMs": 700 },
|
||||
|
||||
"hint": {
|
||||
"enabled": true,
|
||||
"rows": 24,
|
||||
"textCase": "upper",
|
||||
"fontPx": 32,
|
||||
"opacity": 0.02,
|
||||
"angleDeg": -12,
|
||||
"rowGapPx": 8,
|
||||
"spacing": "\u00A0",
|
||||
"planeScale": 3,
|
||||
"offsetAmpPct": 35,
|
||||
"featherPct": 0
|
||||
},
|
||||
|
||||
"tiles": [
|
||||
{
|
||||
"title": "YouTube",
|
||||
"href": "https://www.youtube.com/lasereverything",
|
||||
"icon": "/icons/youtube.png",
|
||||
"color": "#2563eb",
|
||||
"size": "2x2",
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "YouTube, the world's second largest search engine."
|
||||
},
|
||||
{
|
||||
"title": "Facebook Page",
|
||||
"href": "https://www.facebook.com/lasereverythingofficial/",
|
||||
"icon": "/icons/facebook-p.png",
|
||||
"color": "#2563eb",
|
||||
"size": "2x2",
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "How your represented a business before Facebook Groups were coopted."
|
||||
},
|
||||
{
|
||||
"title": "Facebook Group",
|
||||
"href": "https://www.facebook.com/groups/1047701605814427/",
|
||||
"icon": "/icons/facebook-g.png",
|
||||
"color": "#f39a31",
|
||||
"size": 1,
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "How you represent a business now that Facebook Groups are coopted."
|
||||
},
|
||||
{
|
||||
"title": "Instagram",
|
||||
"href": "https://www.instagram.com/lasereverythingofficial/",
|
||||
"icon": "/icons/instagram.png",
|
||||
"color": "#0f766e",
|
||||
"size": 1,
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "The highlight reels of peoples lives and businesses!"
|
||||
},
|
||||
{
|
||||
"title": "Tiktok",
|
||||
"href": "https://www.tiktok.com/@lasereverythingofficial",
|
||||
"icon": "/icons/tiktok.png",
|
||||
"color": "#0f766e",
|
||||
"size": 1,
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "Banned, unbanned, banned, unbanned - pick a legality and stick with it."
|
||||
},
|
||||
{
|
||||
"title": "Fediverse",
|
||||
"href": "https://makearmy.io",
|
||||
"icon": "/icons/fediverse.png",
|
||||
"image": "",
|
||||
"color": "#134823",
|
||||
"size": "2x1",
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "We run a wide range of fediverse instances. Check out the list at MakeArmy!"
|
||||
}
|
||||
]
|
||||
}
|
||||
63
configs/support.json
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"brand": "LASER EVERYTHING",
|
||||
"grid": { "gap": 12, "fit": "stretch" },
|
||||
"reveal": { "durationMs": 2600, "revertDelayMs": 700 },
|
||||
|
||||
"hint": {
|
||||
"enabled": true,
|
||||
"rows": 24,
|
||||
"textCase": "upper",
|
||||
"fontPx": 32,
|
||||
"opacity": 0.02,
|
||||
"angleDeg": -12,
|
||||
"rowGapPx": 8,
|
||||
"spacing": "\u00A0",
|
||||
"planeScale": 3,
|
||||
"offsetAmpPct": 35,
|
||||
"featherPct": 0
|
||||
},
|
||||
|
||||
"tiles": [
|
||||
{
|
||||
"title": "Laser Master Academy",
|
||||
"href": "https://masters.lasereverything.net",
|
||||
"icon": "/icons/lma.png",
|
||||
"color": "#2563eb",
|
||||
"size": "2x2",
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "Support Laser Everything and hang with true laser masters to hone your skills."
|
||||
},
|
||||
{
|
||||
"title": "Patreon",
|
||||
"href": "https://www.patreon.com/LaserEverything",
|
||||
"icon": "/icons/patreon.png",
|
||||
"color": "#f39a31",
|
||||
"size": 1,
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "Mainstream supporter platform for creators."
|
||||
},
|
||||
{
|
||||
"title": "KoFi",
|
||||
"href": "https://ko-fi.com/lasereverything",
|
||||
"icon": "/icons/kofi.png",
|
||||
"color": "#0f766e",
|
||||
"size": 1,
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "Small time unmanaged funding for one time or repeated donations."
|
||||
},
|
||||
{
|
||||
"title": "Liberapay",
|
||||
"href": "https://liberapay.com/LaserEverything",
|
||||
"icon": "/icons/liberapay.png",
|
||||
"image": "",
|
||||
"color": "#134823",
|
||||
"size": "2x1",
|
||||
"bgFit": "cover",
|
||||
"bgPos": "center",
|
||||
"description": "FOSS and Privacy First supporter platform for creators."
|
||||
}
|
||||
]
|
||||
}
|
||||
155
ethics.html
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Ethics · LaserEverything</title>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
<body class="page">
|
||||
<div class="page-wrap">
|
||||
<header class="page-header">
|
||||
<a class="back-btn" href="/" id="backLink" aria-label="Back to home">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M19 12H5"></path><path d="M12 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
<span>Back to Home</span>
|
||||
</a>
|
||||
<div></div>
|
||||
</header>
|
||||
|
||||
<main class="page-main" role="main">
|
||||
<article class="article">
|
||||
<h1>Ethics</h1>
|
||||
|
||||
<h2>Parties</h2>
|
||||
<ul class="party-list">
|
||||
<li><strong>Laser Everything LLC</strong> (“Laser Everything”)</li>
|
||||
<li><strong>The Partner</strong></li>
|
||||
</ul>
|
||||
|
||||
<h2>Review Ethics Statement</h2>
|
||||
|
||||
<h3>Paid Reviews and Sponsorships</h3>
|
||||
<p>
|
||||
Laser Everything is a viewer-supported channel and is dedicated to remaining unbiased and
|
||||
neutral when presenting information and results uncovered during testing and review procedures.
|
||||
When publishing reviews, Laser Everything does not perform marketing services in exchange for
|
||||
equipment, goods, software, services, or monetary compensation. The Partner agrees to send
|
||||
equipment, goods, software, or services for the sole purpose of review and exposure.
|
||||
Sponsorships are rare; when Laser Everything cooperates with a sponsor, the sponsorship is
|
||||
always clearly disclosed and will never be presented in a way that suggests the content is not sponsored.
|
||||
</p>
|
||||
|
||||
<h3>Editing and Censorship</h3>
|
||||
<p>
|
||||
Laser Everything will provide its viewership with the unedited and uncensored opinion formed
|
||||
after having spent time with the equipment, goods, software, or services. The Partner
|
||||
understands and acknowledges that at no time will a preview copy of content or material be
|
||||
provided, and revisions will not be made to published or unpublished content.
|
||||
</p>
|
||||
|
||||
<h2>Partner Acknowledgements</h2>
|
||||
|
||||
<h3>Expectation of Sales</h3>
|
||||
<p>
|
||||
The Partner provides all equipment, goods, software, or services without the expectation that
|
||||
viewership will proceed with any purchase. Laser Everything makes no guarantees that its
|
||||
viewership will make any purchase and commits to no additional advertising or promotion on
|
||||
behalf of the Partner.
|
||||
</p>
|
||||
|
||||
<h3>Date of Publication</h3>
|
||||
<p>
|
||||
Laser Everything makes no claims or guarantees that content will be published within a specific
|
||||
time period. The Partner acknowledges the publication date of all content is at the sole
|
||||
discretion of Laser Everything.
|
||||
</p>
|
||||
|
||||
<h3>Decision to Publish</h3>
|
||||
<p>
|
||||
Laser Everything makes no claims or guarantees that content will be published featuring any
|
||||
provided equipment, goods, software, or services. The Partner acknowledges the decision to
|
||||
publish any and all content is at the sole discretion of Laser Everything.
|
||||
</p>
|
||||
|
||||
<h3>Liability Release</h3>
|
||||
<p>
|
||||
The Partner acknowledges and understands that all content—published or unpublished—by Laser
|
||||
Everything is presented solely as the opinion of the reviewer. The Partner releases and forever
|
||||
discharges Laser Everything, its owners, directors, officers, employees, agents, assigns, legal
|
||||
representatives, and successors from all manner of actions, causes of action, debts, accounts,
|
||||
bonds, contracts, claims, and demands for or by reason of any damage, loss, or injury to person
|
||||
or property which has or may be sustained as a consequence of any content produced that has
|
||||
been published or remains unpublished.
|
||||
</p>
|
||||
|
||||
<h2>Content Release</h2>
|
||||
<div class="callout legal">
|
||||
<p><strong>The Partner may choose or is currently engaged</strong> in the business of creating media, which includes (but is not limited to):</p>
|
||||
<ul>
|
||||
<li>Filming, film/video editing, and film/video production</li>
|
||||
<li>Photography, photo editing, and photo production</li>
|
||||
<li>Digital photography, digital photo editing, and digital photo production</li>
|
||||
<li>Documentary production and editing</li>
|
||||
<li>Sound recordings; sound manipulation and music productions</li>
|
||||
<li>Television production</li>
|
||||
<li>Web design and production</li>
|
||||
</ul>
|
||||
<p>
|
||||
The Partner consents to its equipment, goods, software, or services being a subject of Laser
|
||||
Everything in media, and will allow Laser Everything to capture images and sound recordings
|
||||
for use in media.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>Whereby: Laser Everything is Released of Liability</h2>
|
||||
<p>
|
||||
For good and valuable consideration herein acknowledged as received, the Partner releases Laser
|
||||
Everything and assigns permission to license all images and sound recordings and to use them in
|
||||
any media for any purpose, which may include—among others—advertising, promotion, marketing,
|
||||
and packaging for any product or service. The Partner agrees that any images and sound
|
||||
recordings may be combined with other images, text, and graphics, and may be cropped, altered,
|
||||
and modified.
|
||||
</p>
|
||||
|
||||
<h2>Laser Everything LLC Retains All Rights</h2>
|
||||
<p>
|
||||
The Partner agrees that Laser Everything has all rights to images and sound recordings, for
|
||||
perpetuity. The Partner acknowledges and agrees that Laser Everything is not liable for any
|
||||
further consideration, accounting, or claim for any reason.
|
||||
</p>
|
||||
|
||||
<h2>Duration of Agreement</h2>
|
||||
<p>
|
||||
The Partner acknowledges and agrees that this Agreement is binding on all heirs and assigns.
|
||||
The Partner acknowledges and agrees that this Agreement is irrevocable, worldwide, and
|
||||
perpetual. This Agreement contains the entire agreement between the parties to this release,
|
||||
and the terms of this Agreement are contractual and not a mere recital. This Agreement will be
|
||||
construed in accordance with and governed by the laws of the State of New York.
|
||||
</p>
|
||||
|
||||
<div class="sig">
|
||||
<div><strong>Laser Everything</strong></div>
|
||||
<div>ALEXANDER SELLITE</div>
|
||||
<div>Member</div>
|
||||
<div>Laser Everything LLC</div>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const link = document.getElementById('backLink');
|
||||
link.addEventListener('click', function (e) {
|
||||
try {
|
||||
const sameOrigin = document.referrer && new URL(document.referrer).origin === location.origin;
|
||||
if (sameOrigin && history.length > 1) { e.preventDefault(); history.back(); }
|
||||
} catch(_) {}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
icons/bg.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
icons/bgrm.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
icons/castopod.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
icons/db-c.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
icons/db-cg.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
icons/db-f.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
icons/db-l.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
icons/db-m.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
icons/db-mc.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
icons/db-p.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
icons/db-u.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
icons/db.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
icons/deck.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
icons/dpi.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
icons/ergo.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
icons/ethics.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
icons/facebook-g.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
icons/facebook-p.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
icons/fediverse.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
icons/fs.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
icons/instagram.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
icons/interval.png
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
icons/kofi.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
icons/lemmy.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
icons/liberapay.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
icons/lma.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
icons/makearmy.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
icons/mastodon.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
icons/matrix.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
icons/news.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
icons/nextcloud.png
Normal file
|
After Width: | Height: | Size: 8 KiB |
BIN
icons/patreon.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
icons/peertube.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
icons/picsur.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
icons/pixelfed.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
icons/privatebin.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
icons/support.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
icons/tiktok.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
icons/toolkit.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
icons/training.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
icons/youtube.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
295
index.html
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Laser Everything</title>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="site-header" id="siteHeader" role="banner">
|
||||
<a class="brand" href="/">LASER EVERYTHING</a>
|
||||
</header>
|
||||
|
||||
<main id="grid" class="grid" aria-label="Masonry menu"></main>
|
||||
|
||||
<script>
|
||||
/* ======================================================
|
||||
Deterministic tiles + “wrapping paper” hints (SVG-free)
|
||||
Hint rows are long text lines on a huge plane; tile clips.
|
||||
Config default: /configs/index.json (override with ?config=name)
|
||||
====================================================== */
|
||||
|
||||
(async () => {
|
||||
/* ---------- tiny seeded RNG (deterministic) ---------- */
|
||||
function xmur3(str){
|
||||
let h = 1779033703 ^ str.length;
|
||||
for (let i=0;i<str.length;i++){
|
||||
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
|
||||
h = (h<<13) | (h>>>19);
|
||||
}
|
||||
return function(){
|
||||
h = Math.imul(h ^ (h>>>16), 2246822507);
|
||||
h = Math.imul(h ^ (h>>>13), 3266489909);
|
||||
return (h ^= h>>>16) >>> 0;
|
||||
};
|
||||
}
|
||||
function mulberry32(a){
|
||||
return function(){
|
||||
let t = a += 0x6D2B79F5;
|
||||
t = Math.imul(t ^ (t>>>15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t>>>7), t | 61);
|
||||
return ((t ^ (t>>>14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
const rngFrom = (seedStr) => mulberry32(xmur3(seedStr)());
|
||||
|
||||
/* ---------- load config ---------- */
|
||||
function resolveConfigPath(){
|
||||
const q = new URLSearchParams(location.search).get('config');
|
||||
let p = q || '/configs/index.json';
|
||||
if (!/^https?:\/\//i.test(p) && !p.startsWith('/')) p = '/configs/'+p;
|
||||
if (!/\.json(\?|$)/i.test(p)) p += '.json';
|
||||
const u = new URL(p, location.origin);
|
||||
if (u.origin !== location.origin) throw new Error('Cross-origin config not allowed');
|
||||
return u.pathname + u.search;
|
||||
}
|
||||
|
||||
// Default config (simple & clear). Your JSON can override any/all.
|
||||
let cfg = {
|
||||
brand: "LASER EVERYTHING",
|
||||
grid: { gap: 12, fit: "stretch" }, // fit: 'stretch' | 'fitCells'
|
||||
reveal: { durationMs: 2600, revertDelayMs: 700 },
|
||||
|
||||
hint: {
|
||||
enabled: true,
|
||||
rows: 5, // number of rows
|
||||
textCase: "upper", // 'upper' | 'lower' | 'none'
|
||||
fontPx: 16, // base size in px
|
||||
opacity: 0.12,
|
||||
angleDeg: -12, // rotation of the hint plane
|
||||
rowGapPx: 8, // vertical gap between rows
|
||||
spacing: "\u00A0\u00A0\u00A0",// between repeats (use NBSP or your string)
|
||||
planeScale: 3, // plane size vs tile (3 = 300% both axes)
|
||||
offsetAmpPct: 35, // max per-row horizontal offset (% of tile width)
|
||||
featherPct: 0 // 0..10 soft mask on extreme left/right (optional)
|
||||
},
|
||||
|
||||
tiles: []
|
||||
};
|
||||
|
||||
let CONFIG_URL = '/configs/index.json';
|
||||
try { CONFIG_URL = resolveConfigPath(); } catch {}
|
||||
try {
|
||||
const r = await fetch(CONFIG_URL, { cache:'no-store' });
|
||||
if (r.ok) Object.assign(cfg, await r.json());
|
||||
} catch {}
|
||||
|
||||
/* ---------- apply brand + CSS vars ---------- */
|
||||
if (cfg.brand) document.querySelector('.brand').textContent = cfg.brand;
|
||||
if (cfg.grid?.gap != null) document.documentElement.style.setProperty('--gap', (cfg.grid.gap|0)+'px');
|
||||
if (cfg.reveal?.durationMs != null) document.documentElement.style.setProperty('--reveal-ms', (cfg.reveal.durationMs|0)+'ms');
|
||||
|
||||
// Hint CSS vars
|
||||
const H = cfg.hint || {};
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--hint-opacity', String(H.opacity ?? 0.12));
|
||||
root.style.setProperty('--hint-angle', (H.angleDeg!=null ? H.angleDeg : -12) + 'deg');
|
||||
root.style.setProperty('--hint-size', (H.fontPx!=null ? (H.fontPx|0)+'px' : '16px'));
|
||||
root.style.setProperty('--hint-row-gap', (H.rowGapPx!=null ? (H.rowGapPx|0)+'px' : '8px'));
|
||||
root.style.setProperty('--hint-plane', (H.planeScale!=null ? +H.planeScale : 3));
|
||||
|
||||
const grid = document.getElementById('grid');
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
function parseSpan(v){
|
||||
if (typeof v==='number') return {w:v,h:v};
|
||||
if (typeof v==='string'){ const m=v.toLowerCase().match(/^(\d+)x(\d+)$/); if (m) return {w:+m[1], h:+m[2]}; }
|
||||
if (v && typeof v==='object') return {w:Math.max(1,+v.w||1), h:Math.max(1,+v.h||1)};
|
||||
return {w:1,h:1};
|
||||
}
|
||||
function titleForHint(title){
|
||||
if (!title) return '';
|
||||
const mode = (cfg.hint?.textCase ?? 'upper').toLowerCase();
|
||||
if (mode === 'upper') return title.toUpperCase();
|
||||
if (mode === 'lower') return title.toLowerCase();
|
||||
return title;
|
||||
}
|
||||
|
||||
/* ---------- build tiles ---------- */
|
||||
const frag = document.createDocumentFragment();
|
||||
const tiles = [];
|
||||
|
||||
(cfg.tiles||[]).forEach((t,i)=>{
|
||||
const a = document.createElement('a');
|
||||
a.className='tile';
|
||||
a.href = t.href || '#';
|
||||
a.setAttribute('aria-label', t.title || ('Tile ' + (i+1)));
|
||||
if (t.color) a.style.setProperty('--color', t.color);
|
||||
|
||||
if (t.image){
|
||||
const src=(t.image.startsWith('/')||t.image.startsWith('http')) ? t.image : ('/images/'+t.image);
|
||||
a.style.backgroundImage='url("'+src+'")';
|
||||
a.style.backgroundSize=t.bgFit||'cover';
|
||||
a.style.backgroundRepeat='no-repeat';
|
||||
a.style.backgroundPosition=t.bgPos||'center';
|
||||
}
|
||||
|
||||
// Wipe cover
|
||||
const cover = document.createElement('div'); cover.className='cover'; a.appendChild(cover);
|
||||
|
||||
// ===== Hint layer (novel approach) =====
|
||||
if ((cfg.hint?.enabled!==false) && (t.hintTitle || t.title)){
|
||||
const hintLayer = document.createElement('div');
|
||||
hintLayer.className = 'hint-layer';
|
||||
if ((cfg.hint?.featherPct|0) > 0){
|
||||
hintLayer.style.setProperty('--hint-feather', (cfg.hint.featherPct|0) + '%');
|
||||
}
|
||||
|
||||
const stack = document.createElement('div');
|
||||
stack.className = 'hint-stack';
|
||||
stack.style.setProperty('--plane-scale', String(cfg.hint?.planeScale ?? 3));
|
||||
hintLayer.appendChild(stack);
|
||||
|
||||
// Prepare deterministic RNG per tile
|
||||
const seed = 'row|' + (t.title || ('tile'+i));
|
||||
const rnd = rngFrom(seed);
|
||||
|
||||
// Compose very long content (no measuring).
|
||||
const baseTitle = titleForHint(t.hintTitle || t.title || '');
|
||||
const spacing = (cfg.hint?.spacing ?? '\u00A0\u00A0\u00A0');
|
||||
const chunk = (baseTitle + spacing);
|
||||
const repeats = 240; // big enough that it ALWAYS bleeds past edges
|
||||
const lineText = chunk.repeat(repeats);
|
||||
|
||||
// Build rows, centered in the big plane, with deterministic offsets.
|
||||
const rows = Math.max(1, cfg.hint?.rows|0 || 3);
|
||||
for (let r=0; r<rows; r++){
|
||||
const row = document.createElement('div');
|
||||
row.className = 'hint-row';
|
||||
row.textContent = lineText;
|
||||
|
||||
// offset (px) = (random[-1..1] * amp% * tileWidth)
|
||||
// we read tile width later (in layout), so set a data attribute now:
|
||||
const offs = (rnd()*2 - 1) * ((cfg.hint?.offsetAmpPct ?? 35)/100);
|
||||
row.dataset.offsetScale = String(offs); // fraction of tile width
|
||||
|
||||
stack.appendChild(row);
|
||||
}
|
||||
|
||||
a.appendChild(hintLayer);
|
||||
}
|
||||
|
||||
// Icon layer
|
||||
const iconL=document.createElement('div'); iconL.className='layer icon-layer';
|
||||
if (t.icon){
|
||||
if (/\.(png|svg|jpg|jpeg|webp|gif)$/i.test(t.icon)) {
|
||||
const img=document.createElement('img'); img.className='icon'; img.alt=''; img.src=t.icon; iconL.appendChild(img);
|
||||
} else {
|
||||
const span=document.createElement('span'); span.className='icon icon-text'; span.textContent=String(t.icon); iconL.appendChild(span);
|
||||
}
|
||||
}
|
||||
a.appendChild(iconL);
|
||||
|
||||
// Text layer
|
||||
const textL=document.createElement('div'); textL.className='layer text-layer';
|
||||
const wrap=document.createElement('div');
|
||||
const k=document.createElement('div'); k.className='kicker'; k.textContent=t.kicker||''; if(!k.textContent) k.style.display='none'; wrap.appendChild(k);
|
||||
const h2=document.createElement('h2'); h2.className='title'; h2.textContent=t.title||('Tile '+(i+1)); wrap.appendChild(h2);
|
||||
const d=document.createElement('p'); d.className='desc'; d.textContent=((t.description??t.desc)??''); if(!d.textContent.trim()) d.style.display='none'; wrap.appendChild(d);
|
||||
textL.appendChild(wrap); a.appendChild(textL);
|
||||
|
||||
// Sizing
|
||||
const span = parseSpan(t.size || 1);
|
||||
a.dataset.w = String(Math.max(1, Math.min(6, span.w)));
|
||||
a.dataset.h = String(Math.max(1, Math.min(6, span.h)));
|
||||
|
||||
frag.appendChild(a);
|
||||
tiles.push(a);
|
||||
});
|
||||
|
||||
grid.textContent = '';
|
||||
grid.appendChild(frag);
|
||||
|
||||
/* ---------- layout (header-aware, deterministic wipe) ---------- */
|
||||
function layout(){
|
||||
const W = innerWidth;
|
||||
const H = innerHeight;
|
||||
const V = visualViewport;
|
||||
const vh = (V && V.height) ? V.height : H;
|
||||
|
||||
const header = document.getElementById('siteHeader');
|
||||
const HH = Math.max(0, vh - (header ? header.getBoundingClientRect().height : 0));
|
||||
|
||||
const gap = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--gap')) || 0;
|
||||
const fit = (cfg.grid?.fit || 'stretch').toLowerCase();
|
||||
|
||||
const S = tiles.reduce((s, el)=> s + (+el.dataset.w * +el.dataset.h), 0) || 1;
|
||||
const A = W / (HH || 1);
|
||||
let cols = Math.max(1, Math.round(Math.sqrt(S * A)));
|
||||
let rows = Math.max(1, Math.ceil(S / cols));
|
||||
|
||||
const unitFor = (c, r) => Math.max(1, Math.min(
|
||||
Math.floor((W - gap*(c+1)) / c),
|
||||
Math.floor((HH - gap*(r+1)) / r)
|
||||
));
|
||||
|
||||
let best = { cols, rows, unit: unitFor(cols, rows) };
|
||||
for (let c=Math.max(1, cols-3); c<=cols+3; c++){
|
||||
const r = Math.max(1, Math.ceil(S / c));
|
||||
const u = unitFor(c, r);
|
||||
if (u > best.unit || (u === best.unit && r < best.rows)) best = { cols:c, rows:r, unit:u };
|
||||
}
|
||||
|
||||
grid.style.gap = gap + 'px';
|
||||
grid.style.padding = gap + 'px';
|
||||
|
||||
if (fit === 'stretch'){
|
||||
const unitW = (W - gap * (best.cols + 1)) / best.cols;
|
||||
const unitH = (HH - gap * (best.rows + 1)) / best.rows;
|
||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unitW}px)`;
|
||||
grid.style.gridAutoRows = `${unitH}px`;
|
||||
} else {
|
||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${best.unit}px)`;
|
||||
grid.style.gridAutoRows = `${best.unit}px`;
|
||||
}
|
||||
|
||||
tiles.forEach(n=>{
|
||||
n.style.gridColumn = `span ${n.dataset.w}`;
|
||||
n.style.gridRow = `span ${n.dataset.h}`;
|
||||
|
||||
// Resolve deterministic wipe dir (seeded by title)
|
||||
const title = n.querySelector('.title')?.textContent || '';
|
||||
const R = rngFrom('wipe|'+title);
|
||||
const dirs = [[1,0],[-1,0],[0,-1],[0,1],[1,-1],[-1,-1],[-1,1],[1,1]];
|
||||
const [dx,dy] = dirs[Math.floor(R()*dirs.length)];
|
||||
n.style.setProperty('--dx', (dx*140)+'%');
|
||||
n.style.setProperty('--dy', (dy*140)+'%');
|
||||
|
||||
// Update row pixel offsets based on current tile width
|
||||
const tw = n.getBoundingClientRect().width;
|
||||
n.querySelectorAll('.hint-row').forEach(row=>{
|
||||
const scale = parseFloat(row.dataset.offsetScale || '0'); // [-1..1] * amp%
|
||||
const px = scale * tw; // px offset
|
||||
row.style.setProperty('--row-offset-px', px + 'px');
|
||||
});
|
||||
});
|
||||
}
|
||||
layout();
|
||||
const resched = () => { clearTimeout(layout._t); layout._t=setTimeout(layout, 50); };
|
||||
addEventListener('resize', resched);
|
||||
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
|
||||
|
||||
// JS-assisted reveal class only
|
||||
tiles.forEach(tile=>{
|
||||
tile.addEventListener('mouseenter',()=>tile.classList.add('js-reveal'));
|
||||
tile.addEventListener('mouseleave',()=>tile.classList.remove('js-reveal'));
|
||||
tile.addEventListener('focusin', ()=>tile.classList.add('js-reveal'));
|
||||
tile.addEventListener('focusout', ()=>tile.classList.remove('js-reveal'));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
295
social.html
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Laser Everything</title>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="site-header" id="siteHeader" role="banner">
|
||||
<a class="brand" href="/">LASER EVERYTHING</a>
|
||||
</header>
|
||||
|
||||
<main id="grid" class="grid" aria-label="Masonry menu"></main>
|
||||
|
||||
<script>
|
||||
/* ======================================================
|
||||
Deterministic tiles + “wrapping paper” hints (SVG-free)
|
||||
Hint rows are long text lines on a huge plane; tile clips.
|
||||
Config default: /configs/index.json (override with ?config=name)
|
||||
====================================================== */
|
||||
|
||||
(async () => {
|
||||
/* ---------- tiny seeded RNG (deterministic) ---------- */
|
||||
function xmur3(str){
|
||||
let h = 1779033703 ^ str.length;
|
||||
for (let i=0;i<str.length;i++){
|
||||
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
|
||||
h = (h<<13) | (h>>>19);
|
||||
}
|
||||
return function(){
|
||||
h = Math.imul(h ^ (h>>>16), 2246822507);
|
||||
h = Math.imul(h ^ (h>>>13), 3266489909);
|
||||
return (h ^= h>>>16) >>> 0;
|
||||
};
|
||||
}
|
||||
function mulberry32(a){
|
||||
return function(){
|
||||
let t = a += 0x6D2B79F5;
|
||||
t = Math.imul(t ^ (t>>>15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t>>>7), t | 61);
|
||||
return ((t ^ (t>>>14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
const rngFrom = (seedStr) => mulberry32(xmur3(seedStr)());
|
||||
|
||||
/* ---------- load config ---------- */
|
||||
function resolveConfigPath(){
|
||||
const q = new URLSearchParams(location.search).get('config');
|
||||
let p = q || '/configs/social.json';
|
||||
if (!/^https?:\/\//i.test(p) && !p.startsWith('/')) p = '/configs/'+p;
|
||||
if (!/\.json(\?|$)/i.test(p)) p += '.json';
|
||||
const u = new URL(p, location.origin);
|
||||
if (u.origin !== location.origin) throw new Error('Cross-origin config not allowed');
|
||||
return u.pathname + u.search;
|
||||
}
|
||||
|
||||
// Default config (simple & clear). Your JSON can override any/all.
|
||||
let cfg = {
|
||||
brand: "LASER EVERYTHING",
|
||||
grid: { gap: 12, fit: "stretch" }, // fit: 'stretch' | 'fitCells'
|
||||
reveal: { durationMs: 2600, revertDelayMs: 700 },
|
||||
|
||||
hint: {
|
||||
enabled: true,
|
||||
rows: 5, // number of rows
|
||||
textCase: "upper", // 'upper' | 'lower' | 'none'
|
||||
fontPx: 16, // base size in px
|
||||
opacity: 0.12,
|
||||
angleDeg: -12, // rotation of the hint plane
|
||||
rowGapPx: 8, // vertical gap between rows
|
||||
spacing: "\u00A0\u00A0\u00A0",// between repeats (use NBSP or your string)
|
||||
planeScale: 3, // plane size vs tile (3 = 300% both axes)
|
||||
offsetAmpPct: 35, // max per-row horizontal offset (% of tile width)
|
||||
featherPct: 0 // 0..10 soft mask on extreme left/right (optional)
|
||||
},
|
||||
|
||||
tiles: []
|
||||
};
|
||||
|
||||
let CONFIG_URL = '/configs/index.json';
|
||||
try { CONFIG_URL = resolveConfigPath(); } catch {}
|
||||
try {
|
||||
const r = await fetch(CONFIG_URL, { cache:'no-store' });
|
||||
if (r.ok) Object.assign(cfg, await r.json());
|
||||
} catch {}
|
||||
|
||||
/* ---------- apply brand + CSS vars ---------- */
|
||||
if (cfg.brand) document.querySelector('.brand').textContent = cfg.brand;
|
||||
if (cfg.grid?.gap != null) document.documentElement.style.setProperty('--gap', (cfg.grid.gap|0)+'px');
|
||||
if (cfg.reveal?.durationMs != null) document.documentElement.style.setProperty('--reveal-ms', (cfg.reveal.durationMs|0)+'ms');
|
||||
|
||||
// Hint CSS vars
|
||||
const H = cfg.hint || {};
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--hint-opacity', String(H.opacity ?? 0.12));
|
||||
root.style.setProperty('--hint-angle', (H.angleDeg!=null ? H.angleDeg : -12) + 'deg');
|
||||
root.style.setProperty('--hint-size', (H.fontPx!=null ? (H.fontPx|0)+'px' : '16px'));
|
||||
root.style.setProperty('--hint-row-gap', (H.rowGapPx!=null ? (H.rowGapPx|0)+'px' : '8px'));
|
||||
root.style.setProperty('--hint-plane', (H.planeScale!=null ? +H.planeScale : 3));
|
||||
|
||||
const grid = document.getElementById('grid');
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
function parseSpan(v){
|
||||
if (typeof v==='number') return {w:v,h:v};
|
||||
if (typeof v==='string'){ const m=v.toLowerCase().match(/^(\d+)x(\d+)$/); if (m) return {w:+m[1], h:+m[2]}; }
|
||||
if (v && typeof v==='object') return {w:Math.max(1,+v.w||1), h:Math.max(1,+v.h||1)};
|
||||
return {w:1,h:1};
|
||||
}
|
||||
function titleForHint(title){
|
||||
if (!title) return '';
|
||||
const mode = (cfg.hint?.textCase ?? 'upper').toLowerCase();
|
||||
if (mode === 'upper') return title.toUpperCase();
|
||||
if (mode === 'lower') return title.toLowerCase();
|
||||
return title;
|
||||
}
|
||||
|
||||
/* ---------- build tiles ---------- */
|
||||
const frag = document.createDocumentFragment();
|
||||
const tiles = [];
|
||||
|
||||
(cfg.tiles||[]).forEach((t,i)=>{
|
||||
const a = document.createElement('a');
|
||||
a.className='tile';
|
||||
a.href = t.href || '#';
|
||||
a.setAttribute('aria-label', t.title || ('Tile ' + (i+1)));
|
||||
if (t.color) a.style.setProperty('--color', t.color);
|
||||
|
||||
if (t.image){
|
||||
const src=(t.image.startsWith('/')||t.image.startsWith('http')) ? t.image : ('/images/'+t.image);
|
||||
a.style.backgroundImage='url("'+src+'")';
|
||||
a.style.backgroundSize=t.bgFit||'cover';
|
||||
a.style.backgroundRepeat='no-repeat';
|
||||
a.style.backgroundPosition=t.bgPos||'center';
|
||||
}
|
||||
|
||||
// Wipe cover
|
||||
const cover = document.createElement('div'); cover.className='cover'; a.appendChild(cover);
|
||||
|
||||
// ===== Hint layer (novel approach) =====
|
||||
if ((cfg.hint?.enabled!==false) && (t.hintTitle || t.title)){
|
||||
const hintLayer = document.createElement('div');
|
||||
hintLayer.className = 'hint-layer';
|
||||
if ((cfg.hint?.featherPct|0) > 0){
|
||||
hintLayer.style.setProperty('--hint-feather', (cfg.hint.featherPct|0) + '%');
|
||||
}
|
||||
|
||||
const stack = document.createElement('div');
|
||||
stack.className = 'hint-stack';
|
||||
stack.style.setProperty('--plane-scale', String(cfg.hint?.planeScale ?? 3));
|
||||
hintLayer.appendChild(stack);
|
||||
|
||||
// Prepare deterministic RNG per tile
|
||||
const seed = 'row|' + (t.title || ('tile'+i));
|
||||
const rnd = rngFrom(seed);
|
||||
|
||||
// Compose very long content (no measuring).
|
||||
const baseTitle = titleForHint(t.hintTitle || t.title || '');
|
||||
const spacing = (cfg.hint?.spacing ?? '\u00A0\u00A0\u00A0');
|
||||
const chunk = (baseTitle + spacing);
|
||||
const repeats = 240; // big enough that it ALWAYS bleeds past edges
|
||||
const lineText = chunk.repeat(repeats);
|
||||
|
||||
// Build rows, centered in the big plane, with deterministic offsets.
|
||||
const rows = Math.max(1, cfg.hint?.rows|0 || 3);
|
||||
for (let r=0; r<rows; r++){
|
||||
const row = document.createElement('div');
|
||||
row.className = 'hint-row';
|
||||
row.textContent = lineText;
|
||||
|
||||
// offset (px) = (random[-1..1] * amp% * tileWidth)
|
||||
// we read tile width later (in layout), so set a data attribute now:
|
||||
const offs = (rnd()*2 - 1) * ((cfg.hint?.offsetAmpPct ?? 35)/100);
|
||||
row.dataset.offsetScale = String(offs); // fraction of tile width
|
||||
|
||||
stack.appendChild(row);
|
||||
}
|
||||
|
||||
a.appendChild(hintLayer);
|
||||
}
|
||||
|
||||
// Icon layer
|
||||
const iconL=document.createElement('div'); iconL.className='layer icon-layer';
|
||||
if (t.icon){
|
||||
if (/\.(png|svg|jpg|jpeg|webp|gif)$/i.test(t.icon)) {
|
||||
const img=document.createElement('img'); img.className='icon'; img.alt=''; img.src=t.icon; iconL.appendChild(img);
|
||||
} else {
|
||||
const span=document.createElement('span'); span.className='icon icon-text'; span.textContent=String(t.icon); iconL.appendChild(span);
|
||||
}
|
||||
}
|
||||
a.appendChild(iconL);
|
||||
|
||||
// Text layer
|
||||
const textL=document.createElement('div'); textL.className='layer text-layer';
|
||||
const wrap=document.createElement('div');
|
||||
const k=document.createElement('div'); k.className='kicker'; k.textContent=t.kicker||''; if(!k.textContent) k.style.display='none'; wrap.appendChild(k);
|
||||
const h2=document.createElement('h2'); h2.className='title'; h2.textContent=t.title||('Tile '+(i+1)); wrap.appendChild(h2);
|
||||
const d=document.createElement('p'); d.className='desc'; d.textContent=((t.description??t.desc)??''); if(!d.textContent.trim()) d.style.display='none'; wrap.appendChild(d);
|
||||
textL.appendChild(wrap); a.appendChild(textL);
|
||||
|
||||
// Sizing
|
||||
const span = parseSpan(t.size || 1);
|
||||
a.dataset.w = String(Math.max(1, Math.min(6, span.w)));
|
||||
a.dataset.h = String(Math.max(1, Math.min(6, span.h)));
|
||||
|
||||
frag.appendChild(a);
|
||||
tiles.push(a);
|
||||
});
|
||||
|
||||
grid.textContent = '';
|
||||
grid.appendChild(frag);
|
||||
|
||||
/* ---------- layout (header-aware, deterministic wipe) ---------- */
|
||||
function layout(){
|
||||
const W = innerWidth;
|
||||
const H = innerHeight;
|
||||
const V = visualViewport;
|
||||
const vh = (V && V.height) ? V.height : H;
|
||||
|
||||
const header = document.getElementById('siteHeader');
|
||||
const HH = Math.max(0, vh - (header ? header.getBoundingClientRect().height : 0));
|
||||
|
||||
const gap = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--gap')) || 0;
|
||||
const fit = (cfg.grid?.fit || 'stretch').toLowerCase();
|
||||
|
||||
const S = tiles.reduce((s, el)=> s + (+el.dataset.w * +el.dataset.h), 0) || 1;
|
||||
const A = W / (HH || 1);
|
||||
let cols = Math.max(1, Math.round(Math.sqrt(S * A)));
|
||||
let rows = Math.max(1, Math.ceil(S / cols));
|
||||
|
||||
const unitFor = (c, r) => Math.max(1, Math.min(
|
||||
Math.floor((W - gap*(c+1)) / c),
|
||||
Math.floor((HH - gap*(r+1)) / r)
|
||||
));
|
||||
|
||||
let best = { cols, rows, unit: unitFor(cols, rows) };
|
||||
for (let c=Math.max(1, cols-3); c<=cols+3; c++){
|
||||
const r = Math.max(1, Math.ceil(S / c));
|
||||
const u = unitFor(c, r);
|
||||
if (u > best.unit || (u === best.unit && r < best.rows)) best = { cols:c, rows:r, unit:u };
|
||||
}
|
||||
|
||||
grid.style.gap = gap + 'px';
|
||||
grid.style.padding = gap + 'px';
|
||||
|
||||
if (fit === 'stretch'){
|
||||
const unitW = (W - gap * (best.cols + 1)) / best.cols;
|
||||
const unitH = (HH - gap * (best.rows + 1)) / best.rows;
|
||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unitW}px)`;
|
||||
grid.style.gridAutoRows = `${unitH}px`;
|
||||
} else {
|
||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${best.unit}px)`;
|
||||
grid.style.gridAutoRows = `${best.unit}px`;
|
||||
}
|
||||
|
||||
tiles.forEach(n=>{
|
||||
n.style.gridColumn = `span ${n.dataset.w}`;
|
||||
n.style.gridRow = `span ${n.dataset.h}`;
|
||||
|
||||
// Resolve deterministic wipe dir (seeded by title)
|
||||
const title = n.querySelector('.title')?.textContent || '';
|
||||
const R = rngFrom('wipe|'+title);
|
||||
const dirs = [[1,0],[-1,0],[0,-1],[0,1],[1,-1],[-1,-1],[-1,1],[1,1]];
|
||||
const [dx,dy] = dirs[Math.floor(R()*dirs.length)];
|
||||
n.style.setProperty('--dx', (dx*140)+'%');
|
||||
n.style.setProperty('--dy', (dy*140)+'%');
|
||||
|
||||
// Update row pixel offsets based on current tile width
|
||||
const tw = n.getBoundingClientRect().width;
|
||||
n.querySelectorAll('.hint-row').forEach(row=>{
|
||||
const scale = parseFloat(row.dataset.offsetScale || '0'); // [-1..1] * amp%
|
||||
const px = scale * tw; // px offset
|
||||
row.style.setProperty('--row-offset-px', px + 'px');
|
||||
});
|
||||
});
|
||||
}
|
||||
layout();
|
||||
const resched = () => { clearTimeout(layout._t); layout._t=setTimeout(layout, 50); };
|
||||
addEventListener('resize', resched);
|
||||
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
|
||||
|
||||
// JS-assisted reveal class only
|
||||
tiles.forEach(tile=>{
|
||||
tile.addEventListener('mouseenter',()=>tile.classList.add('js-reveal'));
|
||||
tile.addEventListener('mouseleave',()=>tile.classList.remove('js-reveal'));
|
||||
tile.addEventListener('focusin', ()=>tile.classList.add('js-reveal'));
|
||||
tile.addEventListener('focusout', ()=>tile.classList.remove('js-reveal'));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
193
styles.css
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
:root{
|
||||
--bg:#121212;
|
||||
--fg:#eceff3;
|
||||
--gap:12px;
|
||||
--reveal-ms:2600ms;
|
||||
--header-h:56px;
|
||||
--safe-top: env(safe-area-inset-top, 0px);
|
||||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
|
||||
/* Hint knobs (wired from config.hint) */
|
||||
--hint-opacity:.12;
|
||||
--hint-angle:-12deg;
|
||||
--hint-size:16px;
|
||||
--hint-row-gap:8px;
|
||||
--hint-plane:3;
|
||||
--hint-feather:0%;
|
||||
}
|
||||
|
||||
/* ===== Base ===== */
|
||||
*{ box-sizing:border-box }
|
||||
html,body{ height:100% }
|
||||
body{
|
||||
margin:0; color:var(--fg); background:var(--bg);
|
||||
font:16px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial;
|
||||
overflow:hidden; /* tiles view */
|
||||
}
|
||||
a{ color:inherit; text-decoration:none }
|
||||
|
||||
/* ===== Header ===== */
|
||||
.site-header{
|
||||
position:fixed; left:0; right:0; top:0;
|
||||
height:calc(var(--header-h) + var(--safe-top));
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
padding:0 clamp(12px,3vw,24px);
|
||||
background:rgba(0,0,0,.55); backdrop-filter:blur(6px);
|
||||
border-bottom:1px solid rgba(255,255,255,.10);
|
||||
z-index:2000;
|
||||
}
|
||||
.site-header .brand{
|
||||
font-weight:900; letter-spacing:.22em; text-transform:uppercase;
|
||||
font-size:clamp(12px,1.4vw,14px); opacity:.95; text-decoration:none;
|
||||
}
|
||||
|
||||
/* ===== Grid ===== */
|
||||
.grid{
|
||||
position:fixed; inset:0;
|
||||
top:calc(var(--header-h) + var(--safe-top));
|
||||
display:grid; grid-auto-flow:dense;
|
||||
grid-template-columns:repeat(12, 80px);
|
||||
grid-auto-rows:80px;
|
||||
gap:var(--gap); padding:var(--gap);
|
||||
}
|
||||
|
||||
/* ===== Tiles ===== */
|
||||
.tile{
|
||||
position:relative; display:block; overflow:hidden;
|
||||
background:var(--color,#1f2937);
|
||||
border:1px solid rgba(255,255,255,.08);
|
||||
transition:opacity .55s ease, transform .55s ease;
|
||||
}
|
||||
.cover{
|
||||
position:absolute; inset:0; z-index:2; background:#242424;
|
||||
transform:translate(0,0);
|
||||
transition:transform var(--reveal-ms) cubic-bezier(.22,.8,.18,1);
|
||||
pointer-events:none; will-change:transform;
|
||||
}
|
||||
.layer,.cover{
|
||||
position:absolute; inset:0; display:grid; place-items:center; justify-items:center;
|
||||
text-align:center; padding:clamp(12px,3vh,28px);
|
||||
transition:opacity .55s ease, transform .55s ease; pointer-events:none;
|
||||
}
|
||||
.text-layer{ z-index:1; opacity:0; transform:translateY(8px) }
|
||||
.icon-layer{ z-index:4; opacity:1 }
|
||||
.icon{ width:clamp(36px,9vh,110px); height:auto; object-fit:contain }
|
||||
.icon.icon-text{ width:auto; font-size:clamp(24px,10vh,120px); line-height:1 }
|
||||
.kicker{ font-weight:900; letter-spacing:.22em; text-transform:uppercase; font-size:12px; opacity:.95; margin:0 0 .25em }
|
||||
.title{ margin:0; font-weight:1000; letter-spacing:.2px; font-size:clamp(16px,2.2vw,28px); line-height:1.1 }
|
||||
.desc{ margin:.45em 0 0; opacity:.92; font-size:14px; max-width:min(72ch,90%) }
|
||||
.text-layer>div{ display:flex; flex-direction:column; align-items:center }
|
||||
|
||||
/* Reveal states */
|
||||
.tile:hover .cover,
|
||||
.tile:focus-visible .cover,
|
||||
.tile.js-reveal .cover{ transform:translate(var(--dx),var(--dy)) }
|
||||
.tile:hover .icon-layer,
|
||||
.tile:focus-visible .icon-layer,
|
||||
.tile.js-reveal .icon-layer{ opacity:0; transform:scale(.965) }
|
||||
.tile:hover .text-layer,
|
||||
.tile:focus-visible .text-layer,
|
||||
.tile.js-reveal .text-layer{ opacity:1; transform:none }
|
||||
|
||||
@media (prefers-reduced-motion:reduce){
|
||||
.tile,.cover,.layer{ transition:none }
|
||||
}
|
||||
|
||||
/* ===== Hint layer (novel approach) =====
|
||||
* A huge “plane” rotated & centered. Rows are long lines of text.
|
||||
* The tile clips the plane, so text can be sliced mid-word at edges. */
|
||||
.hint-layer{
|
||||
position:absolute; inset:0; z-index:3; pointer-events:none;
|
||||
display:block; opacity:var(--hint-opacity);
|
||||
/* optional soft fade at extreme sides */
|
||||
-webkit-mask-image:linear-gradient(to right,
|
||||
rgba(0,0,0,0) var(--hint-feather),
|
||||
#000 calc(var(--hint-feather) + 1px),
|
||||
#000 calc(100% - var(--hint-feather) - 1px),
|
||||
rgba(0,0,0,0) calc(100% - var(--hint-feather)));
|
||||
mask-image:linear-gradient(to right,
|
||||
rgba(0,0,0,0) var(--hint-feather),
|
||||
#000 calc(var(--hint-feather) + 1px),
|
||||
#000 calc(100% - var(--hint-feather) - 1px),
|
||||
rgba(0,0,0,0) calc(100% - var(--hint-feather)));
|
||||
}
|
||||
.hint-stack{
|
||||
position:absolute; left:50%; top:50%;
|
||||
transform:translate(-50%,-50%) rotate(var(--hint-angle));
|
||||
width:calc(var(--hint-plane) * 100%);
|
||||
height:calc(var(--hint-plane) * 100%);
|
||||
display:flex; flex-direction:column; justify-content:center; align-items:center;
|
||||
gap:var(--hint-row-gap);
|
||||
overflow:hidden; /* clip rows to plane; tile clips plane */
|
||||
}
|
||||
.hint-row{
|
||||
position:relative; display:block;
|
||||
/* make the line VERY long; no measuring required */
|
||||
white-space:nowrap; letter-spacing:.22em; line-height:1;
|
||||
font-weight:900; text-transform:uppercase; font-size:var(--hint-size);
|
||||
transform:translateX(var(--row-offset-px, 0px));
|
||||
/* the content string itself is repeated in JS so it actually fills */
|
||||
}
|
||||
|
||||
/* Hide hint during reveal for the “flip” effect */
|
||||
.tile:hover .hint-layer,
|
||||
.tile:focus-visible .hint-layer,
|
||||
.tile.js-reveal .hint-layer{ opacity:0 }
|
||||
|
||||
/* ===== Static pages (unchanged from prior work) ===== */
|
||||
body.page{
|
||||
min-height:100vh; margin:0; color:var(--fg); background:var(--bg);
|
||||
overflow-y:auto; overflow-x:hidden;
|
||||
}
|
||||
.page-wrap{
|
||||
box-sizing:border-box; min-height:100svh;
|
||||
padding:var(--gap);
|
||||
padding-top:calc(var(--gap) + var(--header-h) + var(--safe-top));
|
||||
padding-bottom:calc(var(--gap) + var(--safe-bottom));
|
||||
display:grid; grid-template-rows:auto 1fr; gap:var(--gap);
|
||||
}
|
||||
.page-header{ display:flex; align-items:center; justify-content:space-between; gap:var(--gap) }
|
||||
.back-btn,.btn{
|
||||
display:inline-flex; align-items:center; gap:.6rem;
|
||||
border-radius:999px; padding:.6rem .9rem;
|
||||
border:1px solid rgba(255,255,255,0.10);
|
||||
background:rgba(255,255,255,0.04);
|
||||
color:inherit; text-decoration:none; outline:none;
|
||||
transition:background 300ms, transform 160ms;
|
||||
}
|
||||
.btn.primary{ background:rgba(255,255,255,0.08) }
|
||||
.back-btn:hover,.btn:hover{ background:rgba(255,255,255,0.07); transform:translateY(-1px) }
|
||||
.back-btn:focus-visible,.btn:focus-visible{ box-shadow:0 0 0 3px rgba(154,168,255,.45) }
|
||||
.back-btn svg{ width:18px; height:18px; flex:0 0 auto; opacity:.95 }
|
||||
.page-main{ display:grid; place-items:start center }
|
||||
.article{
|
||||
width:min(90ch,100%); line-height:1.6;
|
||||
background:rgba(255,255,255,0.04);
|
||||
border:1px solid rgba(255,255,255,0.10);
|
||||
border-radius:16px;
|
||||
padding:clamp(16px,3vw,28px);
|
||||
}
|
||||
.article h1{ margin:0 0 .35em; font-size:clamp(22px,3.2vw,36px); letter-spacing:.2px; text-align:center }
|
||||
.subtitle{ margin:0 0 1.1em; opacity:.85; text-align:center; font-size:clamp(14px,1.6vw,16px) }
|
||||
.cta-row{ display:flex; flex-wrap:wrap; gap:.6rem; justify-content:center; margin:0 0 1.25em }
|
||||
.callout{ margin:1.2em 0; padding:1rem; border:1px solid rgba(255,255,255,0.10); border-radius:12px; background:rgba(255,255,255,.03) }
|
||||
.callout.warn{ border-color:rgba(255,99,99,.5); background:rgba(255,0,0,.06) }
|
||||
.callout h3{ margin:.2em 0 .6em; font-size:1rem; letter-spacing:.08em; text-transform:uppercase; opacity:.9 }
|
||||
.article h2{ margin:1.6em 0 .6em; font-size:clamp(18px,2.4vw,24px) }
|
||||
.article h3{ margin:1.2em 0 .4em; font-size:clamp(16px,2vw,20px) }
|
||||
.article p{ margin:.8em 0 }
|
||||
.article ul,.article ol{ margin:.8em 0 .8em 1.2em }
|
||||
.article a{ color:inherit; text-decoration:underline; text-underline-offset:2px }
|
||||
.legal{ border-color:rgba(255,255,255,.18) }
|
||||
.sig{ margin-top:1.5em; padding-top:1em; border-top:1px solid rgba(255,255,255,0.10); display:grid; gap:.25rem }
|
||||
.party-list{ display:grid; gap:.35rem; margin:.6rem 0 1rem; padding-left:1.2rem }
|
||||
|
||||
/* ===== Mobile ===== */
|
||||
@media (max-width:480px){
|
||||
:root{ --gap:8px; --header-h:48px }
|
||||
.kicker{ font-size:11px }
|
||||
.title{ font-size:clamp(16px,5.2vw,22px) }
|
||||
.desc{ font-size:13px }
|
||||
.icon{ width:clamp(28px,11vh,90px) }
|
||||
.back-btn,.btn{ padding:.55rem .85rem }
|
||||
}
|
||||
295
support.html
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Laser Everything</title>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="site-header" id="siteHeader" role="banner">
|
||||
<a class="brand" href="/">LASER EVERYTHING</a>
|
||||
</header>
|
||||
|
||||
<main id="grid" class="grid" aria-label="Masonry menu"></main>
|
||||
|
||||
<script>
|
||||
/* ======================================================
|
||||
Deterministic tiles + “wrapping paper” hints (SVG-free)
|
||||
Hint rows are long text lines on a huge plane; tile clips.
|
||||
Config default: /configs/index.json (override with ?config=name)
|
||||
====================================================== */
|
||||
|
||||
(async () => {
|
||||
/* ---------- tiny seeded RNG (deterministic) ---------- */
|
||||
function xmur3(str){
|
||||
let h = 1779033703 ^ str.length;
|
||||
for (let i=0;i<str.length;i++){
|
||||
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
|
||||
h = (h<<13) | (h>>>19);
|
||||
}
|
||||
return function(){
|
||||
h = Math.imul(h ^ (h>>>16), 2246822507);
|
||||
h = Math.imul(h ^ (h>>>13), 3266489909);
|
||||
return (h ^= h>>>16) >>> 0;
|
||||
};
|
||||
}
|
||||
function mulberry32(a){
|
||||
return function(){
|
||||
let t = a += 0x6D2B79F5;
|
||||
t = Math.imul(t ^ (t>>>15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t>>>7), t | 61);
|
||||
return ((t ^ (t>>>14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
const rngFrom = (seedStr) => mulberry32(xmur3(seedStr)());
|
||||
|
||||
/* ---------- load config ---------- */
|
||||
function resolveConfigPath(){
|
||||
const q = new URLSearchParams(location.search).get('config');
|
||||
let p = q || '/configs/support.json';
|
||||
if (!/^https?:\/\//i.test(p) && !p.startsWith('/')) p = '/configs/'+p;
|
||||
if (!/\.json(\?|$)/i.test(p)) p += '.json';
|
||||
const u = new URL(p, location.origin);
|
||||
if (u.origin !== location.origin) throw new Error('Cross-origin config not allowed');
|
||||
return u.pathname + u.search;
|
||||
}
|
||||
|
||||
// Default config (simple & clear). Your JSON can override any/all.
|
||||
let cfg = {
|
||||
brand: "LASER EVERYTHING",
|
||||
grid: { gap: 12, fit: "stretch" }, // fit: 'stretch' | 'fitCells'
|
||||
reveal: { durationMs: 2600, revertDelayMs: 700 },
|
||||
|
||||
hint: {
|
||||
enabled: true,
|
||||
rows: 5, // number of rows
|
||||
textCase: "upper", // 'upper' | 'lower' | 'none'
|
||||
fontPx: 16, // base size in px
|
||||
opacity: 0.12,
|
||||
angleDeg: -12, // rotation of the hint plane
|
||||
rowGapPx: 8, // vertical gap between rows
|
||||
spacing: "\u00A0\u00A0\u00A0",// between repeats (use NBSP or your string)
|
||||
planeScale: 3, // plane size vs tile (3 = 300% both axes)
|
||||
offsetAmpPct: 35, // max per-row horizontal offset (% of tile width)
|
||||
featherPct: 0 // 0..10 soft mask on extreme left/right (optional)
|
||||
},
|
||||
|
||||
tiles: []
|
||||
};
|
||||
|
||||
let CONFIG_URL = '/configs/index.json';
|
||||
try { CONFIG_URL = resolveConfigPath(); } catch {}
|
||||
try {
|
||||
const r = await fetch(CONFIG_URL, { cache:'no-store' });
|
||||
if (r.ok) Object.assign(cfg, await r.json());
|
||||
} catch {}
|
||||
|
||||
/* ---------- apply brand + CSS vars ---------- */
|
||||
if (cfg.brand) document.querySelector('.brand').textContent = cfg.brand;
|
||||
if (cfg.grid?.gap != null) document.documentElement.style.setProperty('--gap', (cfg.grid.gap|0)+'px');
|
||||
if (cfg.reveal?.durationMs != null) document.documentElement.style.setProperty('--reveal-ms', (cfg.reveal.durationMs|0)+'ms');
|
||||
|
||||
// Hint CSS vars
|
||||
const H = cfg.hint || {};
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--hint-opacity', String(H.opacity ?? 0.12));
|
||||
root.style.setProperty('--hint-angle', (H.angleDeg!=null ? H.angleDeg : -12) + 'deg');
|
||||
root.style.setProperty('--hint-size', (H.fontPx!=null ? (H.fontPx|0)+'px' : '16px'));
|
||||
root.style.setProperty('--hint-row-gap', (H.rowGapPx!=null ? (H.rowGapPx|0)+'px' : '8px'));
|
||||
root.style.setProperty('--hint-plane', (H.planeScale!=null ? +H.planeScale : 3));
|
||||
|
||||
const grid = document.getElementById('grid');
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
function parseSpan(v){
|
||||
if (typeof v==='number') return {w:v,h:v};
|
||||
if (typeof v==='string'){ const m=v.toLowerCase().match(/^(\d+)x(\d+)$/); if (m) return {w:+m[1], h:+m[2]}; }
|
||||
if (v && typeof v==='object') return {w:Math.max(1,+v.w||1), h:Math.max(1,+v.h||1)};
|
||||
return {w:1,h:1};
|
||||
}
|
||||
function titleForHint(title){
|
||||
if (!title) return '';
|
||||
const mode = (cfg.hint?.textCase ?? 'upper').toLowerCase();
|
||||
if (mode === 'upper') return title.toUpperCase();
|
||||
if (mode === 'lower') return title.toLowerCase();
|
||||
return title;
|
||||
}
|
||||
|
||||
/* ---------- build tiles ---------- */
|
||||
const frag = document.createDocumentFragment();
|
||||
const tiles = [];
|
||||
|
||||
(cfg.tiles||[]).forEach((t,i)=>{
|
||||
const a = document.createElement('a');
|
||||
a.className='tile';
|
||||
a.href = t.href || '#';
|
||||
a.setAttribute('aria-label', t.title || ('Tile ' + (i+1)));
|
||||
if (t.color) a.style.setProperty('--color', t.color);
|
||||
|
||||
if (t.image){
|
||||
const src=(t.image.startsWith('/')||t.image.startsWith('http')) ? t.image : ('/images/'+t.image);
|
||||
a.style.backgroundImage='url("'+src+'")';
|
||||
a.style.backgroundSize=t.bgFit||'cover';
|
||||
a.style.backgroundRepeat='no-repeat';
|
||||
a.style.backgroundPosition=t.bgPos||'center';
|
||||
}
|
||||
|
||||
// Wipe cover
|
||||
const cover = document.createElement('div'); cover.className='cover'; a.appendChild(cover);
|
||||
|
||||
// ===== Hint layer (novel approach) =====
|
||||
if ((cfg.hint?.enabled!==false) && (t.hintTitle || t.title)){
|
||||
const hintLayer = document.createElement('div');
|
||||
hintLayer.className = 'hint-layer';
|
||||
if ((cfg.hint?.featherPct|0) > 0){
|
||||
hintLayer.style.setProperty('--hint-feather', (cfg.hint.featherPct|0) + '%');
|
||||
}
|
||||
|
||||
const stack = document.createElement('div');
|
||||
stack.className = 'hint-stack';
|
||||
stack.style.setProperty('--plane-scale', String(cfg.hint?.planeScale ?? 3));
|
||||
hintLayer.appendChild(stack);
|
||||
|
||||
// Prepare deterministic RNG per tile
|
||||
const seed = 'row|' + (t.title || ('tile'+i));
|
||||
const rnd = rngFrom(seed);
|
||||
|
||||
// Compose very long content (no measuring).
|
||||
const baseTitle = titleForHint(t.hintTitle || t.title || '');
|
||||
const spacing = (cfg.hint?.spacing ?? '\u00A0\u00A0\u00A0');
|
||||
const chunk = (baseTitle + spacing);
|
||||
const repeats = 240; // big enough that it ALWAYS bleeds past edges
|
||||
const lineText = chunk.repeat(repeats);
|
||||
|
||||
// Build rows, centered in the big plane, with deterministic offsets.
|
||||
const rows = Math.max(1, cfg.hint?.rows|0 || 3);
|
||||
for (let r=0; r<rows; r++){
|
||||
const row = document.createElement('div');
|
||||
row.className = 'hint-row';
|
||||
row.textContent = lineText;
|
||||
|
||||
// offset (px) = (random[-1..1] * amp% * tileWidth)
|
||||
// we read tile width later (in layout), so set a data attribute now:
|
||||
const offs = (rnd()*2 - 1) * ((cfg.hint?.offsetAmpPct ?? 35)/100);
|
||||
row.dataset.offsetScale = String(offs); // fraction of tile width
|
||||
|
||||
stack.appendChild(row);
|
||||
}
|
||||
|
||||
a.appendChild(hintLayer);
|
||||
}
|
||||
|
||||
// Icon layer
|
||||
const iconL=document.createElement('div'); iconL.className='layer icon-layer';
|
||||
if (t.icon){
|
||||
if (/\.(png|svg|jpg|jpeg|webp|gif)$/i.test(t.icon)) {
|
||||
const img=document.createElement('img'); img.className='icon'; img.alt=''; img.src=t.icon; iconL.appendChild(img);
|
||||
} else {
|
||||
const span=document.createElement('span'); span.className='icon icon-text'; span.textContent=String(t.icon); iconL.appendChild(span);
|
||||
}
|
||||
}
|
||||
a.appendChild(iconL);
|
||||
|
||||
// Text layer
|
||||
const textL=document.createElement('div'); textL.className='layer text-layer';
|
||||
const wrap=document.createElement('div');
|
||||
const k=document.createElement('div'); k.className='kicker'; k.textContent=t.kicker||''; if(!k.textContent) k.style.display='none'; wrap.appendChild(k);
|
||||
const h2=document.createElement('h2'); h2.className='title'; h2.textContent=t.title||('Tile '+(i+1)); wrap.appendChild(h2);
|
||||
const d=document.createElement('p'); d.className='desc'; d.textContent=((t.description??t.desc)??''); if(!d.textContent.trim()) d.style.display='none'; wrap.appendChild(d);
|
||||
textL.appendChild(wrap); a.appendChild(textL);
|
||||
|
||||
// Sizing
|
||||
const span = parseSpan(t.size || 1);
|
||||
a.dataset.w = String(Math.max(1, Math.min(6, span.w)));
|
||||
a.dataset.h = String(Math.max(1, Math.min(6, span.h)));
|
||||
|
||||
frag.appendChild(a);
|
||||
tiles.push(a);
|
||||
});
|
||||
|
||||
grid.textContent = '';
|
||||
grid.appendChild(frag);
|
||||
|
||||
/* ---------- layout (header-aware, deterministic wipe) ---------- */
|
||||
function layout(){
|
||||
const W = innerWidth;
|
||||
const H = innerHeight;
|
||||
const V = visualViewport;
|
||||
const vh = (V && V.height) ? V.height : H;
|
||||
|
||||
const header = document.getElementById('siteHeader');
|
||||
const HH = Math.max(0, vh - (header ? header.getBoundingClientRect().height : 0));
|
||||
|
||||
const gap = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--gap')) || 0;
|
||||
const fit = (cfg.grid?.fit || 'stretch').toLowerCase();
|
||||
|
||||
const S = tiles.reduce((s, el)=> s + (+el.dataset.w * +el.dataset.h), 0) || 1;
|
||||
const A = W / (HH || 1);
|
||||
let cols = Math.max(1, Math.round(Math.sqrt(S * A)));
|
||||
let rows = Math.max(1, Math.ceil(S / cols));
|
||||
|
||||
const unitFor = (c, r) => Math.max(1, Math.min(
|
||||
Math.floor((W - gap*(c+1)) / c),
|
||||
Math.floor((HH - gap*(r+1)) / r)
|
||||
));
|
||||
|
||||
let best = { cols, rows, unit: unitFor(cols, rows) };
|
||||
for (let c=Math.max(1, cols-3); c<=cols+3; c++){
|
||||
const r = Math.max(1, Math.ceil(S / c));
|
||||
const u = unitFor(c, r);
|
||||
if (u > best.unit || (u === best.unit && r < best.rows)) best = { cols:c, rows:r, unit:u };
|
||||
}
|
||||
|
||||
grid.style.gap = gap + 'px';
|
||||
grid.style.padding = gap + 'px';
|
||||
|
||||
if (fit === 'stretch'){
|
||||
const unitW = (W - gap * (best.cols + 1)) / best.cols;
|
||||
const unitH = (HH - gap * (best.rows + 1)) / best.rows;
|
||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unitW}px)`;
|
||||
grid.style.gridAutoRows = `${unitH}px`;
|
||||
} else {
|
||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${best.unit}px)`;
|
||||
grid.style.gridAutoRows = `${best.unit}px`;
|
||||
}
|
||||
|
||||
tiles.forEach(n=>{
|
||||
n.style.gridColumn = `span ${n.dataset.w}`;
|
||||
n.style.gridRow = `span ${n.dataset.h}`;
|
||||
|
||||
// Resolve deterministic wipe dir (seeded by title)
|
||||
const title = n.querySelector('.title')?.textContent || '';
|
||||
const R = rngFrom('wipe|'+title);
|
||||
const dirs = [[1,0],[-1,0],[0,-1],[0,1],[1,-1],[-1,-1],[-1,1],[1,1]];
|
||||
const [dx,dy] = dirs[Math.floor(R()*dirs.length)];
|
||||
n.style.setProperty('--dx', (dx*140)+'%');
|
||||
n.style.setProperty('--dy', (dy*140)+'%');
|
||||
|
||||
// Update row pixel offsets based on current tile width
|
||||
const tw = n.getBoundingClientRect().width;
|
||||
n.querySelectorAll('.hint-row').forEach(row=>{
|
||||
const scale = parseFloat(row.dataset.offsetScale || '0'); // [-1..1] * amp%
|
||||
const px = scale * tw; // px offset
|
||||
row.style.setProperty('--row-offset-px', px + 'px');
|
||||
});
|
||||
});
|
||||
}
|
||||
layout();
|
||||
const resched = () => { clearTimeout(layout._t); layout._t=setTimeout(layout, 50); };
|
||||
addEventListener('resize', resched);
|
||||
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
|
||||
|
||||
// JS-assisted reveal class only
|
||||
tiles.forEach(tile=>{
|
||||
tile.addEventListener('mouseenter',()=>tile.classList.add('js-reveal'));
|
||||
tile.addEventListener('mouseleave',()=>tile.classList.remove('js-reveal'));
|
||||
tile.addEventListener('focusin', ()=>tile.classList.add('js-reveal'));
|
||||
tile.addEventListener('focusout', ()=>tile.classList.remove('js-reveal'));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
98
training.html
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Laser Engraving Training · LaserEverything</title>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
<body class="page">
|
||||
<div class="page-wrap">
|
||||
<header class="page-header">
|
||||
<a class="back-btn" href="/" id="backLink" aria-label="Back to home">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M19 12H5"></path><path d="M12 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
<span>Back to Home</span>
|
||||
</a>
|
||||
<div></div>
|
||||
</header>
|
||||
|
||||
<main class="page-main" role="main">
|
||||
<article class="article">
|
||||
<h1>Laser Engraving Training</h1>
|
||||
<p class="subtitle"><strong>ONE</strong> Maker <strong>TWO</strong> Specialists | <strong>ZERO</strong> Issues</p>
|
||||
|
||||
<div class="cta-row">
|
||||
<a class="btn primary" href="mailto:?subject=Book%20Coaching%20Session">Book With Us</a>
|
||||
<a class="btn" href="mailto:?subject=Book%20Coaching%20Session">Send Us an Email</a>
|
||||
</div>
|
||||
|
||||
<p><em>Send Us an Email</em> with “<strong>Book Coaching Session</strong>” in the subject. Review this page and, in the body, include what you’d like to cover and what you’re hoping to achieve. Please allow at least 24 hours for a response.</p>
|
||||
|
||||
<h2>We’re Here to Help</h2>
|
||||
<p>Don’t go it alone. The staff at Laser Everything are here to help you get rolling on your laser journey. Use the following to help estimate how much time you’ll need to complete your tasks and get your questions answered.</p>
|
||||
|
||||
<div class="callout warn">
|
||||
<h3>Important Note</h3>
|
||||
<p><strong>We do not cover the following in our coaching sessions:</strong></p>
|
||||
<ul>
|
||||
<li>3D or 2.5D–specific functionality (including software Z-axis and 3D scanners)</li>
|
||||
<li>3D software troubleshooting and setup (including EzCad 3 and LenMark 3DS)<br>
|
||||
<small><em>Does not apply to those with a 3D controller paired with a 2D scanhead, or LightBurn height-map engraving.</em></small>
|
||||
</li>
|
||||
<li>Materials that we deem dangerous — see the <a href="/material-safety.html">Material Safety Lookup</a>.</li>
|
||||
</ul>
|
||||
<p><strong>Refunds will not be issued</strong> to individuals who ignore this warning. If you have questions, please email us <strong>before</strong> purchase.</p>
|
||||
</div>
|
||||
|
||||
<h2>Come Prepared</h2>
|
||||
<p>How to make the most out of your coaching session’s allotted time.</p>
|
||||
|
||||
<h3>Learn Google Meet</h3>
|
||||
<p>
|
||||
Google Meet lets us talk, see each other, and share screens to diagnose issues or teach workflows.
|
||||
If you’ve never used it, get familiar ahead of time so we can focus on your goals:
|
||||
<a href="https://support.google.com/meet/">Start Learning Meet</a>.
|
||||
</p>
|
||||
|
||||
<h3>The Right System</h3>
|
||||
<p>
|
||||
Please join the Meet from the device you use to operate your laser so we can see what you see in real time.
|
||||
Screen sharing requires your permission and is vital to providing optimal support. If that’s not possible,
|
||||
you may join from a phone, though it’s discouraged.
|
||||
</p>
|
||||
|
||||
<h3>Hardware Ready</h3>
|
||||
<p>
|
||||
Have your laser unpacked and connected, with your computer updated (including any pending software updates).
|
||||
Keep materials on hand (scrap/test stock is great), especially if your session is tied to a specific material or object type.
|
||||
</p>
|
||||
|
||||
<h3>Plan For Your Needs</h3>
|
||||
<p>
|
||||
To maximize your time and value, make a list of outcomes you want, plus any questions you have.
|
||||
Keep your notes handy, and add answers and new questions as we go.
|
||||
It’s the best way to get the most done together—and gives you a reference afterward.
|
||||
</p>
|
||||
|
||||
<h2>Contact Us</h2>
|
||||
<p>Questions about booking? <a href="mailto:?subject=Book%20Coaching%20Session">Send Us an Email</a>.</p>
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const link = document.getElementById('backLink');
|
||||
link.addEventListener('click', function (e) {
|
||||
try {
|
||||
const sameOrigin = document.referrer && new URL(document.referrer).origin === location.origin;
|
||||
if (sameOrigin && history.length > 1) { e.preventDefault(); history.back(); }
|
||||
} catch(_) {}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||