Visualisierung zu Werbezwecken - Visualisierungen für externes Büro
2025
Visualisierung
⠿
✕
FormatAuto16:94:31:13:44:52:3
Bild ändern
Ziehen
09
Neubau Einfamilienhaus
Massivbau · Poroton
2025
Realisiert
⠿
✕
FormatAuto16:94:31:13:44:52:3
Bild ändern
Ziehen
10
Energetische Sanierung und Umbau
Kategorie · Jahr
2026
Entwurf
⠿
✕
FormatAuto16:94:31:13:44:52:3
Bild ändern
Ziehen
11
Sonstiges
Entwurf
⠿
✕
FormatAuto16:94:31:13:44:52:3
Bild ändern
Ziehen
12
Gewändeschalung
Seminar · Proof of Concept
2024
Forschung
⠿
✕
FormatAuto16:94:31:13:44:52:3
Bild ändern
Ziehen
13
Bau einer 3-Achs CNC Fräse aus Holz
Fabrikation · Technische Planung
2023
Realisiert
Zurück
07 / 013Bearbeiten
Noch kein Bild
Bild hochladen / ändern — Ziehen zum Positionieren
Vollbild
Bild ändern
Hero FormatAuto (21:9)16:94:321:92:13:1
Lichtspielhaus
Kategorie · Jahr
Projektbeschreibung hier eingeben.
Weiter lesen
Jahr
2026
Kategorie
Entwurf
Werkzeuge
+ Hinzufügen
Weitere Bilder9 Bilder
01
✕
⠿
Gerendertes Umgebungsmodell
02
✕
⠿
03
✕
⠿
Diagramm der Kinosäle im Baukörper
04
✕
⠿
Diagramm des Tragwerks
05
✕
⠿
Diagramm der Treppen und Fluchtwege
06
✕
⠿
Gerendertes Umgebungsmodell
07
✕
⠿
Modellfoto außen
08
✕
⠿
Modellfoto (Schnitt durch Fluchttreppen hinter der Fassade)
09
✕
⠿
Modellfoto (Schnitt durch kleine Kinosääle)
Bild
Vorher/Nachher
Side-by-Side
Zeichnungen & Pläne9 Dateien
01
✕
⠿
02
✕
⠿
03
✕
⠿
04
✕
⠿
05
✕
⠿
06
✕
⠿
07
✕
⠿
08
✕
⠿
09
✕
⠿
Zeichnung
✕‹
VorherNachher
›
5 / 5(Übung) Entwurf eines Treppenhauses mit Eingangsbereich eines Mehrfamilienhauses
{
const idx=parseInt(c.dataset.idx);
if(isNaN(idx))return;
const p=projects[idx];
if(!c.querySelector('.card-del-btn')){
const btn=document.createElement('button');
btn.className='card-del-btn';btn.textContent='✕';btn.title='Projekt entfernen';
btn.onclick=(e)=>removeProject(idx,e);
c.insertBefore(btn,c.firstChild);
}
if(!c.querySelector('.fmt-panel')){
const ratios=[
{key:'auto',label:'Auto'},{key:'16/9',label:'16:9'},{key:'4/3',label:'4:3'},
{key:'1/1',label:'1:1'},{key:'3/4',label:'3:4'},{key:'4/5',label:'4:5'},{key:'2/3',label:'2:3'}
];
const panel=document.createElement('div');
panel.className='fmt-panel';
const rl=document.createElement('span');rl.className='fmt-label';rl.textContent='Format';
panel.appendChild(rl);
ratios.forEach(r=>{
const btn=document.createElement('button');
btn.className='fmt-btn'+((p.imgRatio||'auto')===r.key?' on':'');
btn.textContent=r.label;
btn.onclick=(e)=>{
e.stopPropagation();p.imgRatio=r.key;applyRatio(idx,r.key);
panel.querySelectorAll('.fmt-btn').forEach(b=>b.classList.remove('on'));
btn.classList.add('on');
};
panel.appendChild(btn);
});
// insert after del btn
c.insertBefore(panel, c.children[1]||c.firstChild);
}
// apply saved ratio
if(p.imgRatio&&p.imgRatio!=='auto'){
const wrap=document.getElementById('thumb-'+idx);
if(wrap)wrap.style.aspectRatio=p.imgRatio;
}
});
// Sync any DOM edits to projects[] before rebuild
syncDomToProjects();
// Rebuild grid to add drag hints and other edit-mode elements
rebuildGrid();
}
function exitEdit(){
// Sync any DOM edits to projects[] before rebuild
syncDomToProjects();
editActive=false;syncEditState();syncCatLabels();
// Rebuild grid to remove edit-mode elements
rebuildGrid();
// Re-check description height after exiting edit mode (layout needs time to update)
requestAnimationFrame(() => {
setTimeout(checkDescriptionHeight, 300);
});
}
// Scans a chunk of text (HTML or JSON) for every embedded base64 image,
// wherever it appears — the hidden data script tag AND any already-rendered
// in the DOM (card thumbnails, hero images, etc.) both
// match this. Each unique image is extracted once (via `map`, so the same
// photo used e.g. as both cover and gallery image is only saved once) and
// replaced in-place with its relative file path.
function extractImagesFromText(text, files, map, counter){
const re = /data:image\/([a-zA-Z0-9.+-]+);base64,[A-Za-z0-9+/=]+/g;
return text.replace(re, (match, fmt) => {
if(map.has(match)) return map.get(match);
let ext = fmt.toLowerCase().replace('jpeg','jpg');
if(ext.length > 5) ext = 'jpg';
const fname = `images/img_${String(counter.n++).padStart(4,'0')}.${ext}`;
const b64 = match.slice(match.indexOf('base64,') + 7);
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
files.push({ name: fname, blob: new Blob([bytes], {type:'image/'+fmt}) });
map.set(match, fname);
return fname;
});
}
function downloadBlob(blob, name){
return new Promise(resolve => {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => { URL.revokeObjectURL(a.href); resolve(); }, 60);
});
}
function sleep(ms){ return new Promise(r => setTimeout(r, ms)); }
async function buildExportPayload(){
syncDomToProjects();
if(document.getElementById('view-project').style.display!=='none')syncProjectToCard(currentProj);
syncCatLabels();
exitEdit();
const files = [];
const map = new Map();
const counter = {n:1};
// The toast/progress overlay are transient UI, driven by inline styles that
// reflect whatever was on screen at this exact moment (e.g. "Bereite
// Dateien vor..." fully visible). If we serialize the DOM as-is, that
// visible state gets baked permanently into the exported static file.
// Detach them for the capture, then put them back for the live session.
const toastEl = document.getElementById('_toast');
const toastParent = toastEl ? toastEl.parentNode : null;
if(toastEl) toastEl.remove();
const progressEl = document.getElementById('upload-progress');
const progressParent = progressEl ? progressEl.parentNode : null;
if(progressEl) progressEl.remove();
// Serialize the page cheaply: temporarily empty the (huge) data script tag
// before reading outerHTML, so we don't scan that same payload twice —
// it still leaves every rendered in the visible DOM
// (cards, thumbnails, hero images, gallery) in the captured HTML.
const dataTag = document.getElementById('projectData');
const originalDataText = dataTag.textContent;
dataTag.textContent = '';
let html = '\n' + document.documentElement.outerHTML;
dataTag.textContent = originalDataText;
if(toastEl && toastParent) toastParent.appendChild(toastEl);
if(progressEl && progressParent) progressParent.appendChild(progressEl);
// Extract every image found anywhere in the rendered page...
html = extractImagesFromText(html, files, map, counter);
// ...then build the slim projects JSON using the SAME map, so identical
// images (e.g. a photo used as both cover and gallery entry) resolve to
// the exact same already-extracted file instead of being saved twice.
let dataJson = JSON.stringify(projects);
dataJson = extractImagesFromText(dataJson, files, map, counter);
dataJson = dataJson.replace(/)([\s\S]*?)(<\/script>)/,
(m, a, b, c) => a + dataJson + c
);
// Bake the currently selected font in as the new default — otherwise the
// exported page falls back to localStorage, which only exists in the
// browser you edited in, not for visitors on the live site.
html = html.replace(
/let currentFont = localStorage\.getItem\('portfolio_font'\) \|\| '[^']*';/,
`let currentFont = localStorage.getItem('portfolio_font') || '${currentFont}';`
);
return { htmlBlob: new Blob([html], {type:'text/html'}), files };
}
async function exportFiles(){
// Ask for the folder FIRST, immediately on click — the browser only allows
// showDirectoryPicker() as a direct response to a user gesture, and any
// heavy work beforehand (this file is 230+ MB) can make it think the
// gesture "expired", auto-cancelling the dialog.
let rootHandle = null;
const hasFolderAccess = 'showDirectoryPicker' in window;
if(hasFolderAccess){
try{
rootHandle = await window.showDirectoryPicker({mode:'readwrite'});
}catch(e){
showToast('Abgebrochen — kein Ordner ausgewählt');
return;
}
}
showToast('⏳ Bereite Dateien vor...');
await sleep(30); // let the toast paint before the heavy work
try{
const { htmlBlob, files } = await buildExportPayload();
if(rootHandle){
showToast(`⏳ Schreibe ${files.length + 1} Dateien in den Ordner...`);
const writeFile = async (dirHandle, name, blob) => {
const fh = await dirHandle.getFileHandle(name, {create:true});
const w = await fh.createWritable();
await w.write(blob);
await w.close();
};
await writeFile(rootHandle, 'index.html', htmlBlob);
const imagesDir = await rootHandle.getDirectoryHandle('images', {create:true});
for(const f of files){
const shortName = f.name.replace(/^images\//,'');
await writeFile(imagesDir, shortName, f.blob);
}
showToast(`✓ Fertig — index.html + Ordner "images" (${files.length} Bilder) wurden in ${rootHandle.name} gespeichert`);
return;
}
// Fallback for browsers without folder access (e.g. Firefox, Safari):
// sequential downloads. The "images/..." filename makes most browsers
// create that subfolder inside the Downloads folder automatically.
showToast(`⏳ Lade ${files.length + 1} Dateien herunter — falls der Browser fragt, bitte "Mehrere Downloads zulassen" bestätigen`);
await sleep(400);
await downloadBlob(htmlBlob, 'index.html');
await sleep(350);
for(const f of files){
await downloadBlob(f.blob, f.name);
await sleep(220); // throttle so the browser doesn't block rapid downloads
}
showToast(`✓ Fertig — index.html + ${files.length} Bilder liegen im Downloads-Ordner (Bilder im Unterordner "images")`);
}catch(err){
console.error('Export fehlgeschlagen', err);
showToast('✕ Export fehlgeschlagen: ' + err.message);
}
}
function saveHTML(){
// Sync every editable data source into the `projects` array, then write it
// into the hidden #projectData script tag. From this point on, the live DOM
// fully reflects the current state — so the browser's own "Save Page As"
// (Strg+S / Cmd+S) captures everything correctly. No custom download needed.
syncDomToProjects();
if(document.getElementById('view-project').style.display!=='none')syncProjectToCard(currentProj);
syncCatLabels();
exitEdit();
syncDataToScript();
showToast('✓ Bereit — Strg+S drücken, dann "Webseite, vollständig" wählen (NICHT "nur HTML")');
}
// Toggle fullscreen mode for lightbox
function toggleFullscreen(){
const lb = document.getElementById('lightbox');
if(!document.fullscreenElement){
// Enter fullscreen
if(lb.requestFullscreen){
lb.requestFullscreen();
} else if(lb.webkitRequestFullscreen){ // Safari
lb.webkitRequestFullscreen();
} else if(lb.msRequestFullscreen){ // IE11
lb.msRequestFullscreen();
}
} else {
// Exit fullscreen
if(document.exitFullscreen){
document.exitFullscreen();
} else if(document.webkitExitFullscreen){ // Safari
document.webkitExitFullscreen();
} else if(document.msExitFullscreen){ // IE11
document.msExitFullscreen();
}
}
}
// Listen for fullscreen changes to update button
document.addEventListener('fullscreenchange', updateFullscreenButton);
document.addEventListener('webkitfullscreenchange', updateFullscreenButton);
document.addEventListener('msfullscreenchange', updateFullscreenButton);
function updateFullscreenButton(){
const btn = document.querySelector('.lb-fullscreen');
if(!btn) return;
if(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement){
// In fullscreen - show exit icon
btn.innerHTML = ``;
btn.title = 'Vollbild verlassen (ESC)';
} else {
// Not in fullscreen - show enter icon
btn.innerHTML = ``;
btn.title = 'Vollbild';
}
}
// Check if description is too long and needs expand button
function checkDescriptionHeight(){
const desc = document.getElementById('p-desc');
const btn = document.getElementById('p-read-more');
// CRITICAL: Reset scroll position to show text from the top!
desc.scrollTop = 0;
// Reset state
desc.classList.remove('expanded');
desc.classList.remove('has-overflow');
// Check if content overflows (scrollHeight > clientHeight means overflow)
if(desc.scrollHeight > desc.clientHeight + 5){ // +5px tolerance
desc.classList.add('has-overflow'); // Show fadeout gradient
btn.classList.remove('hidden'); // Show button (it's visible by default)
btn.textContent = 'Weiter lesen';
} else {
btn.classList.add('hidden'); // Hide button if text is short
}
}
// Toggle description expand/collapse
function toggleDescExpand(){
const desc = document.getElementById('p-desc');
const btn = document.getElementById('p-read-more');
if(desc.classList.contains('expanded')){
// Collapse
desc.classList.remove('expanded');
desc.classList.add('has-overflow'); // Show fadeout again
btn.textContent = 'Weiter lesen';
// Scroll to top of description smoothly
desc.scrollIntoView({behavior: 'smooth', block: 'nearest'});
} else {
// Expand
desc.classList.add('expanded');
desc.classList.remove('has-overflow'); // Hide fadeout when expanded
btn.textContent = 'Weniger anzeigen';
}
}
// Small non-blocking toast notification
function showToast(msg, duration=2800){
let toast=document.getElementById('_toast');
if(!toast){
toast=document.createElement('div');
toast.id='_toast';
toast.style.cssText='position:fixed;bottom:2rem;left:50%;transform:translateX(-50%) translateY(20px);background:var(--ink);color:var(--bg);padding:0.7rem 1.4rem;font-family:var(--font-body);font-size:0.75rem;font-weight:700;letter-spacing:0.08em;z-index:700;opacity:0;transition:all 0.25s ease-out;pointer-events:none;box-shadow:0 4px 20px rgba(0,0,0,0.15)';
document.body.appendChild(toast);
}
toast.textContent=msg;
requestAnimationFrame(()=>{
toast.style.opacity='1';
toast.style.transform='translateX(-50%) translateY(0)';
});
clearTimeout(toast._timer);
toast._timer=setTimeout(()=>{
toast.style.opacity='0';
toast.style.transform='translateX(-50%) translateY(20px)';
}, duration);
}
// Hide edit controls when page is hosted online (not file://)
// This allows you to edit locally but keeps the site clean for visitors
if (window.location.protocol !== 'file:') {
const editBar = document.getElementById('edit-bar');
if (editBar) editBar.style.display = 'none';
const editBtn = document.getElementById('btn-edit');
if (editBtn) editBtn.style.display = 'none';
const editBtnProj = document.getElementById('btn-edit-proj');
if (editBtnProj) editBtnProj.style.display = 'none';
}
// Browser back/forward support: without this, the back button leaves the
// site entirely instead of returning from a project to the overview.
history.replaceState({view:'home'}, '', location.pathname + location.search);
window.addEventListener('popstate', function(e){
const state = e.state;
const lb = document.getElementById('lightbox');
if(state && state.view === 'lightbox'){
lb.classList.add('open');
return;
}
if(lb.classList.contains('open')){
lbCloseInternal();
}
if(state && state.view === 'project' && projects[state.idx]){
showProject(state.idx, true);
} else {
showHome(true);
}
});
// Initialize grid on page load
rebuildGrid();