diff --git a/index.html b/index.html index b899255..d994f40 100644 --- a/index.html +++ b/index.html @@ -2542,16 +2542,14 @@ background: linear-gradient(135deg, #0f3460 0%, #16213e 100%); border: 2px solid #e94560; color: white; - padding: 8px 6px; + padding: 8px 10px; border-radius: 10px; cursor: pointer; transition: all 0.2s; - text-align: center; display: flex; - flex-direction: column; + flex-direction: row; align-items: center; - justify-content: center; - min-height: 70px; + gap: 8px; } .skill-btn:hover:not(:disabled) { background: linear-gradient(135deg, #e94560 0%, #c73e54 100%); @@ -2564,16 +2562,26 @@ transform: none; } .skill-btn .skill-icon-wrapper { - margin-bottom: 3px; + flex-shrink: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; } .skill-btn .skill-icon-wrapper img { - width: 28px; - height: 28px; + width: 32px; + height: 32px; + } + .skill-btn .skill-info { + display: flex; + flex-direction: column; + align-items: flex-start; + min-width: 0; } .skill-btn .skill-name { font-weight: bold; - font-size: 11px; - margin-bottom: 2px; + font-size: 12px; line-height: 1.2; } .skill-btn .skill-cost { @@ -3318,6 +3326,173 @@ to { transform: translateX(100%); opacity: 0; } } + /* Custom Layer & Filter Control */ + .layer-filter-control { + background: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.3); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 13px; + max-height: 80vh; + overflow-y: auto; + } + .layer-filter-toggle { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 20px; + background: white; + border-radius: 8px; + } + .layer-filter-toggle:hover { + background: #f0f0f0; + } + .layer-filter-panel { + display: none; + padding: 12px; + min-width: 220px; + } + .layer-filter-control.expanded .layer-filter-toggle { + display: none; + } + .layer-filter-control.expanded .layer-filter-panel { + display: block; + } + .layer-filter-section { + margin-bottom: 12px; + } + .layer-filter-section:last-child { + margin-bottom: 0; + } + .layer-filter-section-title { + font-weight: 600; + font-size: 11px; + text-transform: uppercase; + color: #666; + margin-bottom: 8px; + padding-bottom: 4px; + border-bottom: 1px solid #eee; + } + .layer-filter-section label { + display: block; + padding: 4px 0; + cursor: pointer; + } + .layer-filter-section label:hover { + background: #f5f5f5; + margin: 0 -8px; + padding: 4px 8px; + border-radius: 4px; + } + .layer-filter-section input[type="radio"] { + margin-right: 8px; + } + .filter-slider-row { + display: flex; + align-items: center; + margin-bottom: 8px; + gap: 8px; + } + .filter-slider-row:last-child { + margin-bottom: 0; + } + .filter-slider-label { + width: 70px; + font-size: 11px; + color: #444; + } + .filter-slider-row input[type="range"] { + flex: 1; + height: 4px; + -webkit-appearance: none; + background: #ddd; + border-radius: 2px; + outline: none; + } + .filter-slider-row input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 14px; + height: 14px; + background: #4CAF50; + border-radius: 50%; + cursor: pointer; + } + .filter-slider-value { + width: 36px; + font-size: 11px; + color: #666; + text-align: right; + } + .filter-checkbox-row { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; + } + .filter-checkbox-row label { + font-size: 11px; + color: #444; + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; + } + .filter-reset-btn { + width: 100%; + margin-top: 10px; + padding: 6px 12px; + background: #f0f0f0; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 11px; + cursor: pointer; + } + .filter-reset-btn:hover { + background: #e0e0e0; + } + .blend-section select { + width: 100%; + padding: 6px 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 12px; + margin-bottom: 8px; + background: white; + } + .blend-section label { + display: block; + font-size: 11px; + color: #444; + margin-bottom: 4px; + } + .blend-section .blend-row { + margin-bottom: 10px; + } + .layer-filter-close { + position: absolute; + top: 8px; + right: 8px; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 14px; + color: #999; + border-radius: 50%; + } + .layer-filter-close:hover { + background: #f0f0f0; + color: #333; + } + .layer-filter-panel { + position: relative; + } + @@ -4183,15 +4358,333 @@ maxNativeZoom: 19 }); + // CartoDB free tile layers + const cartoPositron = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap © CARTO', + subdomains: 'abcd', + maxZoom: 22, + maxNativeZoom: 20 + }); + + const cartoPositronLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap © CARTO', + subdomains: 'abcd', + maxZoom: 22, + maxNativeZoom: 20 + }); + + const cartoVoyager = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap © CARTO', + subdomains: 'abcd', + maxZoom: 22, + maxNativeZoom: 20 + }); + + const cartoVoyagerLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap © CARTO', + subdomains: 'abcd', + maxZoom: 22, + maxNativeZoom: 20 + }); + + const cartoDark = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap © CARTO', + subdomains: 'abcd', + maxZoom: 22, + maxNativeZoom: 20 + }); + + const cartoDarkLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap © CARTO', + subdomains: 'abcd', + maxZoom: 22, + maxNativeZoom: 20 + }); + + // ÖPNVKarte (Public Transport Map) + const opnvKarte = L.tileLayer('https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', { + attribution: 'Map © memomaps.de CC-BY-SA, map data © OpenStreetMap', + maxZoom: 22, + maxNativeZoom: 18 + }); + // Add street map by default streetMap.addTo(map); - // Layer control + // Tile layer configurations (for creating overlay instances) + const tileConfigs = { + "OSM Street": { url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', options: { subdomains: 'abc', maxZoom: 22, maxNativeZoom: 19 } }, + "Positron": { url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', options: { subdomains: 'abcd', maxZoom: 22, maxNativeZoom: 20 } }, + "Positron (No Labels)": { url: 'https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', options: { subdomains: 'abcd', maxZoom: 22, maxNativeZoom: 20 } }, + "Voyager": { url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', options: { subdomains: 'abcd', maxZoom: 22, maxNativeZoom: 20 } }, + "Voyager (No Labels)": { url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png', options: { subdomains: 'abcd', maxZoom: 22, maxNativeZoom: 20 } }, + "Dark": { url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', options: { subdomains: 'abcd', maxZoom: 22, maxNativeZoom: 20 } }, + "Dark (No Labels)": { url: 'https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', options: { subdomains: 'abcd', maxZoom: 22, maxNativeZoom: 20 } }, + "Satellite": { url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', options: { maxZoom: 22, maxNativeZoom: 19 } }, + "Transport (ÖPNV)": { url: 'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', options: { maxZoom: 22, maxNativeZoom: 18 } } + }; + + // Layer instances for base layer switching const baseMaps = { - "Street Map": streetMap, - "Satellite": satellite + "OSM Street": streetMap, + "Positron": cartoPositronLabels, + "Positron (No Labels)": cartoPositron, + "Voyager": cartoVoyagerLabels, + "Voyager (No Labels)": cartoVoyager, + "Dark": cartoDarkLabels, + "Dark (No Labels)": cartoDark, + "Satellite": satellite, + "Transport (ÖPNV)": opnvKarte }; - L.control.layers(baseMaps, null, { position: 'bottomleft' }).addTo(map); + + // Custom Layer & Filter Control + const LayerFilterControl = L.Control.extend({ + options: { position: 'bottomleft' }, + + onAdd: function(map) { + this._map = map; + this._currentLayer = streetMap; + this._overlayLayer = null; + this._filters = { + brightness: 100, + contrast: 100, + saturate: 100, + grayscale: 0, + hueRotate: 0, + invert: false + }; + this._blend = { + overlay: 'none', + opacity: 70, + mode: 'overlay' + }; + + const container = L.DomUtil.create('div', 'layer-filter-control'); + + // Build layer radio buttons HTML + let layerOptionsHtml = ''; + Object.keys(baseMaps).forEach((name, i) => { + const checked = i === 0 ? 'checked' : ''; + layerOptionsHtml += ``; + }); + + // Build overlay layer options + let overlayOptionsHtml = ''; + Object.keys(baseMaps).forEach(name => { + overlayOptionsHtml += ``; + }); + + // Blend modes + const blendModes = ['normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten', + 'color-dodge', 'color-burn', 'hard-light', 'soft-light', + 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity']; + let blendModeOptionsHtml = blendModes.map(m => + `` + ).join(''); + + container.innerHTML = ` +