Commit: b1b0eaa
Parent: d4564be

Add fun logo

Mårten Åsberg committed on 2025-11-19 at 10:37
.dockerignore +3 -0
diff --git a/.dockerignore b/.dockerignore
index aeefa8a..7dce0b2 100644
@@ -6,6 +6,9 @@
# dotenv files
**/.env
# Third-party JavaScript libraries (downloaded during build)
GitBrowser/wwwroot/js/vendor/
# User-specific files
**/*.rsuser
**/*.suo
.gitignore +3 -0
diff --git a/.gitignore b/.gitignore
index cdb9009..5cc5b24 100644
@@ -6,6 +6,9 @@
# dotenv files
.env
# Third-party JavaScript libraries (downloaded during build)
GitBrowser/wwwroot/js/vendor/
# User-specific files
*.rsuser
*.suo
GitBrowser/Components/App.razor +9 -0
diff --git a/GitBrowser/Components/App.razor b/GitBrowser/Components/App.razor
index 0c0a5f9..27a233f 100644
@@ -9,6 +9,14 @@
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["GitBrowser.styles.css"]" />
<ImportMap />
<script type="importmap">
{
"imports": {
"three": "/js/vendor/three.module.js",
"three/addons/": "/js/vendor/"
}
}
</script>
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
@@ -16,6 +24,7 @@
<body>
<Routes />
<script src="@Assets["_framework/blazor.web.js"]"></script>
<script type="module" defer src="/js/logo3d.js"></script>
</body>
</html>
GitBrowser/Components/Layout/MainLayout.razor +3 -4
diff --git a/GitBrowser/Components/Layout/MainLayout.razor b/GitBrowser/Components/Layout/MainLayout.razor
index 9309d04..3d462c4 100644
@@ -4,10 +4,9 @@
<header class="gh-header">
<div class="gh-header-content">
<a href="/" class="gh-logo">
<svg height="32" viewBox="0 0 16 16" width="32" fill="currentColor">
<path
d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z" />
</svg>
<div id="logo-container" class="logo-container">
<img src="/favicon.png" alt="GitBrowser" width="32" height="32" />
</div>
</a>
<SectionOutlet SectionName="header-title" />
</div>
GitBrowser/Components/Layout/MainLayout.razor.css +28 -0
diff --git a/GitBrowser/Components/Layout/MainLayout.razor.css b/GitBrowser/Components/Layout/MainLayout.razor.css
index e4b5bdd..ab79763 100644
@@ -41,6 +41,34 @@
flex-shrink: 0;
}
/* Logo container for both SVG and 3D canvas */
.gh-logo .logo-container {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* 3D Canvas styles */
.gh-logo .logo-container canvas {
display: block;
width: 32px;
height: 32px;
}
/* When 3D is active, prevent opacity change on parent hover */
.gh-logo:has(.logo-3d-active):hover {
opacity: 1;
}
/* User select none for better drag experience */
.gh-logo .logo-container.logo-3d-active {
user-select: none;
-webkit-user-select: none;
}
.gh-header-content ::deep a.gh-title {
color: #ffffff;
font-size: 16px;
GitBrowser/GitBrowser.csproj +1 -0
diff --git a/GitBrowser/GitBrowser.csproj b/GitBrowser/GitBrowser.csproj
index a3506b8..13b4be3 100644
@@ -14,4 +14,5 @@
<ItemGroup>
<ProjectReference Include="..\GitBrowser.SyntaxHighlighter\GitBrowser.SyntaxHighlighter.csproj" />
</ItemGroup>
<Import Project="GitBrowser.targets" />
</Project>
GitBrowser/GitBrowser.targets +64 -0
diff --git a/GitBrowser/GitBrowser.targets b/GitBrowser/GitBrowser.targets
new file mode 100644
index 0000000..9905042
@@ -0,0 +1,64 @@
<Project>
<!-- Download Three.js to source wwwroot directory -->
<Target Name="DownloadThreeJs" BeforeTargets="Build">
<PropertyGroup>
<ThreeJsVersion>0.170.0</ThreeJsVersion>
<VendorDir>$(MSBuildProjectDirectory)\wwwroot\js\vendor</VendorDir>
<ThreeJsFile>$(VendorDir)\three.module.js</ThreeJsFile>
<GLTFLoaderFile>$(VendorDir)\loaders\GLTFLoader.js</GLTFLoaderFile>
<BufferGeometryUtilsFile>$(VendorDir)\utils\BufferGeometryUtils.js</BufferGeometryUtilsFile>
<ThreeJsUrl>https://cdn.jsdelivr.net/npm/three@$(ThreeJsVersion)/build/three.module.js</ThreeJsUrl>
<GLTFLoaderUrl>https://cdn.jsdelivr.net/npm/three@$(ThreeJsVersion)/examples/jsm/loaders/GLTFLoader.js</GLTFLoaderUrl>
<BufferGeometryUtilsUrl>https://cdn.jsdelivr.net/npm/three@$(ThreeJsVersion)/examples/jsm/utils/BufferGeometryUtils.js</BufferGeometryUtilsUrl>
</PropertyGroup>
<!-- Download Three.js core library -->
<Message
Text="Downloading three.module.js to $(VendorDir)..."
Importance="high"
Condition="!Exists('$(ThreeJsFile)')"
/>
<DownloadFile
SourceUrl="$(ThreeJsUrl)"
DestinationFolder="$(VendorDir)"
DestinationFileName="three.module.js"
Condition="!Exists('$(ThreeJsFile)')"
/>
<!-- Download GLTFLoader -->
<Message
Text="Downloading GLTFLoader.js to $(VendorDir)\loader..."
Importance="high"
Condition="!Exists('$(GLTFLoaderFile)')"
/>
<DownloadFile
SourceUrl="$(GLTFLoaderUrl)"
DestinationFolder="$(VendorDir)\loaders"
DestinationFileName="GLTFLoader.js"
Condition="!Exists('$(GLTFLoaderFile)')"
/>
<!-- Download BufferGeometryUtils -->
<Message
Text="Downloading BufferGeometryUtils.js to $(VendorDir)\utils..."
Importance="high"
Condition="!Exists('$(BufferGeometryUtilsFile)')"
/>
<DownloadFile
SourceUrl="$(BufferGeometryUtilsUrl)"
DestinationFolder="$(VendorDir)\utils"
DestinationFileName="BufferGeometryUtils.js"
Condition="!Exists('$(BufferGeometryUtilsFile)')"
/>
<!-- Verify all files exist, fail build if any are missing -->
<Error
Text="Failed to download three.module.js from $(ThreeJsUrl)."
Condition="!Exists('$(ThreeJsFile)')"
/>
<Error
Text="Failed to download GLTFLoader.js from $(GLTFLoaderUrl)."
Condition="!Exists('$(GLTFLoaderFile)')"
/>
<Error
Text="Failed to download BufferGeometryUtils.js from $(BufferGeometryUtilsUrl)."
Condition="!Exists('$(BufferGeometryUtilsFile)')"
/>
</Target>
</Project>
GitBrowser/wwwroot/cardboard-box.svg +14 -0
diff --git a/GitBrowser/wwwroot/cardboard-box.svg b/GitBrowser/wwwroot/cardboard-box.svg
new file mode 100644
index 0000000..0e2c1a7
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<!-- Box outline - isometric 3/4 view -->
<!-- Left face -->
<path d="M2 5 L2 12 L8 14 L8 7 Z" fill="none"/>
<!-- Right face -->
<path d="M8 7 L8 14 L14 12 L14 5 Z" fill="none"/>
<!-- Top face -->
<path d="M2 5 L8 3 L14 5 L8 7 Z" fill="none"/>
<!-- Top flap detail (cardboard box characteristic) -->
<path d="M5 4 L5 6.5"/>
<path d="M11 4 L11 6.5"/>
<!-- Center seam on top -->
<path d="M8 3 L8 7"/>
</svg>
GitBrowser/wwwroot/favicon.png +0 -0
diff --git a/GitBrowser/wwwroot/favicon.png b/GitBrowser/wwwroot/favicon.png
index 8422b59..1e4d42f 100644
Binary files a/GitBrowser/wwwroot/favicon.png and b/GitBrowser/wwwroot/favicon.png differ
GitBrowser/wwwroot/js/logo3d.js +283 -0
diff --git a/GitBrowser/wwwroot/js/logo3d.js b/GitBrowser/wwwroot/js/logo3d.js
new file mode 100644
index 0000000..ec345c1
@@ -0,0 +1,283 @@
// 3D Cardboard Box Logo Module
// Singleton pattern - models loaded once, reused across Blazor navigation
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
// === SINGLETON STATE ===
const state = {
models: { closed: null, open: null },
modelsLoaded: false,
modelsLoading: false,
scene: null,
camera: null,
renderer: null,
currentModel: null,
isHovering: false,
isDragging: false,
previousMouseX: 0,
rotationVelocity: 0,
friction: 0.95,
animationFrameId: null,
container: null,
};
// === INITIALIZATION ===
function initScene() {
if (state.scene) return; // Already initialized
// Scene
state.scene = new THREE.Scene();
// Camera
state.camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
state.camera.position.set(2, 1.5, 3);
state.camera.lookAt(0, 0, 0);
// Renderer
state.renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true,
powerPreference: "low-power",
});
state.renderer.setSize(32, 32);
state.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
state.renderer.setClearColor(0x000000, 0);
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
state.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(2, 3, 2);
state.scene.add(directionalLight);
const fillLight = new THREE.DirectionalLight(0xffffff, 0.3);
fillLight.position.set(-2, 1, -2);
state.scene.add(fillLight);
}
// === MODEL LOADING ===
async function loadModels() {
if (state.modelsLoaded) return; // Already loaded
if (state.modelsLoading) {
// Wait for existing load to complete
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (state.modelsLoaded) {
clearInterval(checkInterval);
resolve();
}
}, 100);
});
}
state.modelsLoading = true;
const loader = new GLTFLoader();
const loadModel = (url) => {
return new Promise((resolve, reject) => {
loader.load(
url,
(gltf) => resolve(gltf.scene),
undefined,
(error) => reject(error)
);
});
};
try {
const [closedModel, openModel] = await Promise.all([
loadModel("/models/box-closed.glb"),
loadModel("/models/box-open.glb"),
]);
// Prepare models - open box scaled larger to compensate for extended flaps
prepareModel(closedModel, 1.0);
prepareModel(openModel, 1.35); // Scale up open box so main body matches closed box
state.models.closed = closedModel;
state.models.open = openModel;
state.modelsLoaded = true;
} catch (error) {
console.error("Failed to load 3D models:", error);
state.modelsLoading = false;
throw error;
}
}
function prepareModel(model, scaleMultiplier = 1.0) {
// First, center all geometries at the origin
model.traverse((child) => {
if (child.isMesh && child.geometry) {
child.geometry.center();
}
});
// Calculate bounding box after centering geometries
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
// Center the entire model
model.position.set(-center.x, -center.y, -center.z);
// Scale to fit with multiplier
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const scale = (1.8 / maxDim) * scaleMultiplier;
model.scale.setScalar(scale);
// Set initial rotation for isometric view
model.rotation.set(0, Math.PI / 4, 0);
}
// === MODEL SWAPPING ===
function swapModel(newModel) {
if (newModel === state.currentModel) return;
// Remove current model
if (state.currentModel) {
const currentRotation = state.currentModel.rotation.clone();
state.scene.remove(state.currentModel);
newModel.rotation.copy(currentRotation);
}
state.currentModel = newModel;
state.scene.add(state.currentModel);
}
// === ANIMATION ===
function animate() {
state.animationFrameId = requestAnimationFrame(animate);
// Apply momentum when not dragging
if (!state.isDragging && Math.abs(state.rotationVelocity) > 0.0001) {
if (state.currentModel) {
state.currentModel.rotation.y += state.rotationVelocity;
}
state.rotationVelocity *= state.friction;
}
if (state.renderer && state.scene && state.camera) {
state.renderer.render(state.scene, state.camera);
}
}
// === EVENT LISTENERS ===
function setupEventListeners(canvas) {
// Hover events for model swap
canvas.addEventListener("mouseenter", () => {
state.isHovering = true;
swapModel(state.models.open);
});
canvas.addEventListener("mouseleave", () => {
state.isHovering = false;
swapModel(state.models.closed);
});
// Drag events for rotation
canvas.addEventListener("mousedown", (e) => {
state.isDragging = true;
state.previousMouseX = e.clientX;
state.rotationVelocity = 0;
e.preventDefault();
});
document.addEventListener("mousemove", (e) => {
if (!state.isDragging) return;
const deltaX = e.clientX - state.previousMouseX;
const rotationAmount = deltaX * 0.01;
if (state.currentModel) {
state.currentModel.rotation.y += rotationAmount;
}
state.rotationVelocity = rotationAmount;
state.previousMouseX = e.clientX;
});
document.addEventListener("mouseup", () => {
if (state.isDragging) {
state.isDragging = false;
}
});
}
// === MAIN REPLACEMENT FUNCTION ===
async function replaceLogo() {
const container = document.getElementById("logo-container");
if (!container) return;
// Check WebGL support
if (!isWebGLAvailable()) {
console.warn("WebGL not available, keeping SVG logo");
return;
}
// Initialize scene if needed
initScene();
// Load models if needed
try {
await loadModels();
} catch (error) {
console.error("Failed to load models, keeping SVG");
return;
}
// Replace content with canvas
container.innerHTML = "";
container.appendChild(state.renderer.domElement);
container.classList.add("logo-3d-active");
// Setup event listeners (only once)
if (!state.currentModel) {
setupEventListeners(state.renderer.domElement);
}
// Show closed box
swapModel(state.models.closed);
// Start animation if not already running
if (!state.animationFrameId) {
animate();
}
}
function isWebGLAvailable() {
try {
const canvas = document.createElement("canvas");
return !!(
window.WebGLRenderingContext &&
(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
);
} catch (e) {
return false;
}
}
// === AUTO INITIALIZATION ===
function init() {
// Initial replacement
replaceLogo();
// Listen for Blazor enhanced navigation
if (window.Blazor) {
window.Blazor.addEventListener("enhancedload", () => {
replaceLogo();
});
}
}
// Run on page load
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => init());
} else {
init();
}
// Export for manual calls if needed
window.replaceLogo3D = replaceLogo;
GitBrowser/wwwroot/models/box-closed.glb +0 -0
diff --git a/GitBrowser/wwwroot/models/box-closed.glb b/GitBrowser/wwwroot/models/box-closed.glb
new file mode 100644
index 0000000..923e235
Binary files /dev/null and b/GitBrowser/wwwroot/models/box-closed.glb differ
GitBrowser/wwwroot/models/box-open.glb +0 -0
diff --git a/GitBrowser/wwwroot/models/box-open.glb b/GitBrowser/wwwroot/models/box-open.glb
new file mode 100644
index 0000000..3ebbf00
Binary files /dev/null and b/GitBrowser/wwwroot/models/box-open.glb differ