Created
June 22, 2025 02:19
-
-
Save cihad/409fbf48cda895b342e4b98fd0b3a1d7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Altıgen mesh oluşturucu | |
// 6 köşeli altıgen için mesh oluşturur | |
const DEFAULT_TEX_COORDS = new Float32Array([ | |
0.5, | |
0.5, // merkez | |
1.0, | |
0.5, // sağ | |
0.75, | |
0.067, // sağ üst | |
0.25, | |
0.067, // sol üst | |
0.0, | |
0.5, // sol | |
0.25, | |
0.933, // sol alt | |
0.75, | |
0.933, // sağ alt | |
]); | |
// Altıgen için triangle fan indisleri | |
const DEFAULT_INDICES = new Uint32Array([ | |
0, | |
1, | |
2, // merkez-sağ-sağüst | |
0, | |
2, | |
3, // merkez-sağüst-solüst | |
0, | |
3, | |
4, // merkez-solüst-sol | |
0, | |
4, | |
5, // merkez-sol-solalt | |
0, | |
5, | |
6, // merkez-solalt-sağalt | |
0, | |
6, | |
1, // merkez-sağalt-sağ | |
]); | |
/* | |
Altıgen köşe düzeni: | |
2 ---- 3 | |
/ \ | |
1 4 | |
\ / | |
6 ---- 5 | |
Merkez: 0 | |
*/ | |
export default function createHexMesh( | |
hexCorners: [number, number, number?][], | |
resolution?: number | |
) { | |
if (!resolution) { | |
return createHexagon(hexCorners); | |
} | |
// Yüksek çözünürlük için tesselation yapılabilir ama şimdilik basit altıgen | |
return createHexagon(hexCorners); | |
} | |
function createHexagon(hexCorners: [number, number, number?][]) { | |
if (hexCorners.length !== 6) { | |
throw new Error("HexBitmapLayer requires exactly 6 corner coordinates"); | |
} | |
// 7 vertex: 6 köşe + 1 merkez | |
const positions = new Float64Array(21); // 7 * 3 | |
// Merkez noktasını hesapla | |
let centerX = 0, | |
centerY = 0, | |
centerZ = 0; | |
for (let i = 0; i < 6; i++) { | |
centerX += hexCorners[i][0]; | |
centerY += hexCorners[i][1]; | |
centerZ += hexCorners[i][2] || 0; | |
} | |
centerX /= 6; | |
centerY /= 6; | |
centerZ /= 6; | |
// Merkez (index 0) | |
positions[0] = centerX; | |
positions[1] = centerY; | |
positions[2] = centerZ; | |
// Köşeler (index 1-6) | |
for (let i = 0; i < 6; i++) { | |
positions[(i + 1) * 3 + 0] = hexCorners[i][0]; | |
positions[(i + 1) * 3 + 1] = hexCorners[i][1]; | |
positions[(i + 1) * 3 + 2] = hexCorners[i][2] || 0; | |
} | |
return { | |
vertexCount: 18, // 6 üçgen * 3 vertex | |
positions, | |
indices: DEFAULT_INDICES, | |
texCoords: DEFAULT_TEX_COORDS, | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// HexBitmapLayer fragment shader | |
/** | |
* Pack the top 12 bits of two normalized floats into 3 8-bit (rgb) values | |
* This enables addressing 4096x4096 individual pixels | |
* | |
* returns vec3 encoded RGB colors | |
* result.r - top 8 bits of u | |
* result.g - top 8 bits of v | |
* result.b - next 4 bits of u and v: (u + v * 16) | |
*/ | |
const packUVsIntoRGB = ` | |
vec3 packUVsIntoRGB(vec2 uv) { | |
// Extract the top 8 bits. We want values to be truncated down so we can add a fraction | |
vec2 uv8bit = floor(uv * 256.); | |
// Calculate the normalized remainders of u and v parts that do not fit into 8 bits | |
// Scale and clamp to 0-1 range | |
vec2 uvFraction = fract(uv * 256.); | |
vec2 uvFraction4bit = floor(uvFraction * 16.); | |
// Remainder can be encoded in blue channel, encode as 4 bits for pixel coordinates | |
float fractions = uvFraction4bit.x + uvFraction4bit.y * 16.; | |
return vec3(uv8bit, fractions) / 255.; | |
} | |
`; | |
export default `\ | |
#version 300 es | |
#define SHADER_NAME hex-bitmap-layer-fragment-shader | |
#ifdef GL_ES | |
precision highp float; | |
#endif | |
uniform sampler2D bitmapTexture; | |
in vec2 vTexCoord; | |
in vec2 vTexPos; | |
out vec4 fragColor; | |
/* projection utils */ | |
const float TILE_SIZE = 512.0; | |
const float PI = 3.1415926536; | |
const float WORLD_SCALE = TILE_SIZE / PI / 2.0; | |
// from degrees to Web Mercator | |
vec2 lnglat_to_mercator(vec2 lnglat) { | |
float x = lnglat.x; | |
float y = clamp(lnglat.y, -89.9, 89.9); | |
return vec2( | |
radians(x) + PI, | |
PI + log(tan(PI * 0.25 + radians(y) * 0.5)) | |
) * WORLD_SCALE; | |
} | |
// from Web Mercator to degrees | |
vec2 mercator_to_lnglat(vec2 xy) { | |
xy /= WORLD_SCALE; | |
return degrees(vec2( | |
xy.x - PI, | |
atan(exp(xy.y - PI)) * 2.0 - PI * 0.5 | |
)); | |
} | |
/* End projection utils */ | |
// apply desaturation | |
vec3 color_desaturate(vec3 color) { | |
float luminance = (color.r + color.g + color.b) * 0.333333333; | |
return mix(color, vec3(luminance), hexBitmap.desaturate); | |
} | |
// apply tint | |
vec3 color_tint(vec3 color) { | |
return color * hexBitmap.tintColor; | |
} | |
// blend with background color | |
vec4 apply_opacity(vec3 color, float alpha) { | |
if (hexBitmap.transparentColor.a == 0.0) { | |
return vec4(color, alpha); | |
} | |
float blendedAlpha = alpha + hexBitmap.transparentColor.a * (1.0 - alpha); | |
float highLightRatio = alpha / blendedAlpha; | |
vec3 blendedRGB = mix(hexBitmap.transparentColor.rgb, color, highLightRatio); | |
return vec4(blendedRGB, blendedAlpha); | |
} | |
// Altıgen içindeki pozisyonu UV koordinatına çevir | |
vec2 getHexUV(vec2 pos) { | |
// Altıgen merkezine göre normalize et | |
vec2 relativePos = pos - hexBitmap.hexCenter; | |
// Altıgen yarıçapına göre normalize et | |
vec2 normalizedPos = relativePos / hexBitmap.hexRadius; | |
// UV koordinatlarını hesapla (merkez 0.5,0.5 olacak şekilde) | |
return vec2( | |
(normalizedPos.x + 1.0) * 0.5, | |
(normalizedPos.y + 1.0) * 0.5 | |
); | |
} | |
${packUVsIntoRGB} | |
void main(void) { | |
vec2 uv = vTexCoord; | |
if (hexBitmap.coordinateConversion < -0.5) { | |
vec2 lnglat = mercator_to_lnglat(vTexPos); | |
uv = getHexUV(lnglat); | |
} else if (hexBitmap.coordinateConversion > 0.5) { | |
vec2 commonPos = lnglat_to_mercator(vTexPos); | |
uv = getHexUV(commonPos); | |
} | |
vec4 bitmapColor = texture(bitmapTexture, uv); | |
fragColor = apply_opacity(color_tint(color_desaturate(bitmapColor.rgb)), bitmapColor.a * layer.opacity); | |
geometry.uv = uv; | |
DECKGL_FILTER_COLOR(fragColor, geometry); | |
if (bool(picking.isActive) && !bool(picking.isAttribute)) { | |
// Since instance information is not used, we can use picking color for pixel index | |
fragColor.rgb = packUVsIntoRGB(uv); | |
} | |
} | |
`; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// HexBitmapLayer için uniform tanımları | |
import type { Texture } from "@luma.gl/core"; | |
import type { ShaderModule } from "@luma.gl/shadertools"; | |
const uniformBlock = `\ | |
uniform hexBitmapUniforms { | |
vec2 hexCorner0; | |
vec2 hexCorner1; | |
vec2 hexCorner2; | |
vec2 hexCorner3; | |
vec2 hexCorner4; | |
vec2 hexCorner5; | |
vec2 hexCenter; | |
float hexRadius; | |
float coordinateConversion; | |
float desaturate; | |
vec3 tintColor; | |
vec4 transparentColor; | |
} hexBitmap; | |
`; | |
export type HexBitmapProps = { | |
hexCorner0: [number, number]; | |
hexCorner1: [number, number]; | |
hexCorner2: [number, number]; | |
hexCorner3: [number, number]; | |
hexCorner4: [number, number]; | |
hexCorner5: [number, number]; | |
hexCenter: [number, number]; | |
hexRadius: number; | |
coordinateConversion: number; | |
desaturate: number; | |
tintColor: [number, number, number]; | |
transparentColor: [number, number, number, number]; | |
bitmapTexture: Texture; | |
}; | |
export const hexBitmapUniforms = { | |
name: "hexBitmap", | |
vs: uniformBlock, | |
fs: uniformBlock, | |
uniformTypes: { | |
hexCorner0: "vec2<f32>", | |
hexCorner1: "vec2<f32>", | |
hexCorner2: "vec2<f32>", | |
hexCorner3: "vec2<f32>", | |
hexCorner4: "vec2<f32>", | |
hexCorner5: "vec2<f32>", | |
hexCenter: "vec2<f32>", | |
hexRadius: "f32", | |
coordinateConversion: "f32", | |
desaturate: "f32", | |
tintColor: "vec3<f32>", | |
transparentColor: "vec4<f32>", | |
}, | |
} as const satisfies ShaderModule<HexBitmapProps>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// HexBitmapLayer vertex shader | |
export default `\ | |
#version 300 es | |
#define SHADER_NAME hex-bitmap-layer-vertex-shader | |
in vec2 texCoords; | |
in vec3 positions; | |
in vec3 positions64Low; | |
out vec2 vTexCoord; | |
out vec2 vTexPos; | |
const vec3 pickingColor = vec3(1.0, 0.0, 0.0); | |
void main(void) { | |
geometry.worldPosition = positions; | |
geometry.uv = texCoords; | |
geometry.pickingColor = pickingColor; | |
gl_Position = project_position_to_clipspace(positions, positions64Low, vec3(0.0), geometry.position); | |
DECKGL_FILTER_GL_POSITION(gl_Position, geometry); | |
vTexCoord = texCoords; | |
if (hexBitmap.coordinateConversion < -0.5) { | |
vTexPos = geometry.position.xy + project.commonOrigin.xy; | |
} else if (hexBitmap.coordinateConversion > 0.5) { | |
vTexPos = geometry.worldPosition.xy; | |
} | |
vec4 color = vec4(0.0); | |
DECKGL_FILTER_COLOR(color, geometry); | |
} | |
`; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @typescript-eslint/no-explicit-any */ | |
// HexBitmapLayer - Altıgen şeklinde bitmap gösterimi | |
import { | |
Layer, | |
project32, | |
picking, | |
type CoordinateSystem, | |
COORDINATE_SYSTEM, | |
type LayerProps, | |
type PickingInfo, | |
type GetPickingInfoParams, | |
type UpdateParameters, | |
type Color, | |
type TextureSource, | |
type Position, | |
type DefaultProps, | |
} from "@deck.gl/core"; | |
import { Model } from "@luma.gl/engine"; | |
import type { SamplerProps, Texture } from "@luma.gl/core"; | |
import createHexMesh from "./create-hex-mesh"; | |
import { | |
hexBitmapUniforms, | |
type HexBitmapProps, | |
} from "./hex-bitmap-layer-uniforms"; | |
import vs from "./hex-bitmap-layer-vertex"; | |
import fs from "./hex-bitmap-layer-fragment"; | |
const defaultProps: DefaultProps<HexBitmapLayerProps> = { | |
image: { type: "image", value: null, async: true }, | |
hexCorners: { | |
type: "array", | |
value: [ | |
[0, 0], | |
[0, 0], | |
[0, 0], | |
[0, 0], | |
[0, 0], | |
[0, 0], | |
], | |
compare: true, | |
}, | |
_imageCoordinateSystem: COORDINATE_SYSTEM.DEFAULT, | |
desaturate: { type: "number", min: 0, max: 1, value: 0 }, | |
transparentColor: { type: "color", value: [0, 0, 0, 0] }, | |
tintColor: { type: "color", value: [255, 255, 255] }, | |
textureParameters: { type: "object", ignore: true, value: null }, | |
}; | |
/** All properties supported by HexBitmapLayer. */ | |
export type HexBitmapLayerProps = _HexBitmapLayerProps & LayerProps; | |
export type HexBitmapCorners = [ | |
Position, | |
Position, | |
Position, | |
Position, | |
Position, | |
Position | |
]; | |
/** Properties added by HexBitmapLayer. */ | |
type _HexBitmapLayerProps = { | |
data: never; | |
/** | |
* The image to display. | |
* | |
* @default null | |
*/ | |
image?: string | TextureSource | null; | |
/** | |
* Coordinates of six corners of the hexagon. | |
* Should follow the sequence of 6 corners starting from right and going clockwise. | |
* Each position could optionally contain a third component `z`. | |
* @default [] | |
*/ | |
hexCorners?: HexBitmapCorners; | |
/** | |
* > Note: this prop is experimental. | |
* | |
* Specifies how image coordinates should be geographically interpreted. | |
* @default COORDINATE_SYSTEM.DEFAULT | |
*/ | |
_imageCoordinateSystem?: CoordinateSystem; | |
/** | |
* The desaturation of the bitmap. Between `[0, 1]`. | |
* @default 0 | |
*/ | |
desaturate?: number; | |
/** | |
* The color to use for transparent pixels, in `[r, g, b, a]`. | |
* @default [0, 0, 0, 0] | |
*/ | |
transparentColor?: Color; | |
/** | |
* The color to tint the bitmap by, in `[r, g, b]`. | |
* @default [255, 255, 255] | |
*/ | |
tintColor?: Color; | |
/** Customize the [texture parameters](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texParameter). */ | |
textureParameters?: SamplerProps | null; | |
}; | |
export type HexBitmapLayerPickingInfo = PickingInfo< | |
null, | |
{ | |
bitmap: { | |
/** Size of the original image */ | |
size: { | |
width: number; | |
height: number; | |
}; | |
/** Hovered pixel uv in 0-1 range */ | |
uv: [number, number]; | |
/** Hovered pixel in the original image */ | |
pixel: [number, number]; | |
} | null; | |
} | |
>; | |
/** Render a bitmap in hexagon shape at specified corners. */ | |
export default class HexBitmapLayer< | |
ExtraPropsT extends Record<string, any> = Record<string, any> | |
> extends Layer<ExtraPropsT & Required<_HexBitmapLayerProps>> { | |
static layerName = "HexBitmapLayer"; | |
static defaultProps = defaultProps; | |
declare state: { | |
disablePicking?: boolean; | |
model?: Model; | |
mesh?: any; | |
coordinateConversion: number; | |
hexCenter: [number, number]; | |
hexRadius: number; | |
hexCorners: [number, number][]; | |
}; | |
getShaders() { | |
return super.getShaders({ | |
vs, | |
fs, | |
modules: [project32, picking, hexBitmapUniforms], | |
}); | |
} | |
initializeState() { | |
const attributeManager = this.getAttributeManager()!; | |
attributeManager.remove(["instancePickingColors"]); | |
const noAlloc = true; | |
attributeManager.add({ | |
indices: { | |
size: 1, | |
isIndexed: true, | |
update: (attribute) => (attribute.value = this.state.mesh.indices), | |
noAlloc, | |
}, | |
positions: { | |
size: 3, | |
type: "float64", | |
fp64: this.use64bitPositions(), | |
update: (attribute) => (attribute.value = this.state.mesh.positions), | |
noAlloc, | |
}, | |
texCoords: { | |
size: 2, | |
update: (attribute) => (attribute.value = this.state.mesh.texCoords), | |
noAlloc, | |
}, | |
}); | |
} | |
updateState(params: UpdateParameters<this>): void { | |
const { props, oldProps, changeFlags } = params; | |
// setup model first | |
const attributeManager = this.getAttributeManager()!; | |
if (changeFlags.extensionsChanged) { | |
this.state.model?.destroy(); | |
this.state.model = this._getModel(); | |
attributeManager.invalidateAll(); | |
} | |
if (props.hexCorners !== oldProps.hexCorners) { | |
const oldMesh = this.state.mesh; | |
const mesh = this._createMesh(); | |
this.state.model!.setVertexCount(mesh.vertexCount); | |
for (const key in mesh) { | |
if ( | |
oldMesh && | |
oldMesh[key as keyof typeof mesh] !== mesh[key as keyof typeof mesh] | |
) { | |
attributeManager.invalidate(key); | |
} | |
} | |
this.setState({ mesh, ...this._getCoordinateUniforms() }); | |
} else if ( | |
props._imageCoordinateSystem !== oldProps._imageCoordinateSystem | |
) { | |
this.setState(this._getCoordinateUniforms()); | |
} | |
} | |
getPickingInfo(params: GetPickingInfoParams): HexBitmapLayerPickingInfo { | |
const { image } = this.props; | |
const info = params.info as HexBitmapLayerPickingInfo; | |
if (!info.color || !image) { | |
info.bitmap = null; | |
return info; | |
} | |
const { width, height } = image as Texture; | |
// Picking color doesn't represent object index in this layer | |
info.index = 0; | |
// Calculate uv and pixel in bitmap | |
const uv = unpackUVsFromRGB(Uint8Array.from(info.color as number[])); | |
info.bitmap = { | |
size: { width, height }, | |
uv, | |
pixel: [Math.floor(uv[0] * width), Math.floor(uv[1] * height)], | |
}; | |
return info; | |
} | |
// Override base Layer multi-depth picking logic | |
disablePickingIndex() { | |
this.setState({ disablePicking: true }); | |
} | |
restorePickingColors() { | |
this.setState({ disablePicking: false }); | |
} | |
protected _updateAutoHighlight(info: PickingInfo) { | |
super._updateAutoHighlight({ | |
...info, | |
color: this.encodePickingColor(0), | |
}); | |
} | |
protected _createMesh() { | |
const { hexCorners } = this.props; | |
if (!hexCorners || hexCorners.length !== 6) { | |
throw new Error("HexBitmapLayer requires exactly 6 corner coordinates"); | |
} | |
// 3D pozisyonları hazırla | |
const corners3D: [number, number, number?][] = hexCorners.map((corner) => [ | |
corner[0], | |
corner[1], | |
corner[2] || 0, | |
]); | |
return createHexMesh(corners3D, this.context.viewport.resolution); | |
} | |
protected _getModel(): Model { | |
return new Model(this.context.device, { | |
...this.getShaders(), | |
id: this.props.id, | |
bufferLayout: this.getAttributeManager()!.getBufferLayouts(), | |
topology: "triangle-list", | |
isInstanced: false, | |
}); | |
} | |
draw(opts: any) { | |
// DrawOptions kaldırıldı, şimdilik any | |
const { shaderModuleProps } = opts; | |
const { | |
model, | |
coordinateConversion, | |
hexCenter, | |
hexRadius, | |
disablePicking, | |
} = this.state; | |
const { image, desaturate, transparentColor, tintColor } = this.props; | |
if (shaderModuleProps.picking.isActive && disablePicking) { | |
return; | |
} | |
// Render the image | |
if (image && model) { | |
const hexBitmapProps: HexBitmapProps = { | |
bitmapTexture: image as Texture, | |
hexCorner0: this.props.hexCorners![0].slice(0, 2) as [number, number], | |
hexCorner1: this.props.hexCorners![1].slice(0, 2) as [number, number], | |
hexCorner2: this.props.hexCorners![2].slice(0, 2) as [number, number], | |
hexCorner3: this.props.hexCorners![3].slice(0, 2) as [number, number], | |
hexCorner4: this.props.hexCorners![4].slice(0, 2) as [number, number], | |
hexCorner5: this.props.hexCorners![5].slice(0, 2) as [number, number], | |
hexCenter: this.state.hexCenter, | |
hexRadius: this.state.hexRadius, | |
coordinateConversion, | |
desaturate, | |
tintColor: tintColor.slice(0, 3).map((x) => x / 255) as [ | |
number, | |
number, | |
number | |
], | |
transparentColor: transparentColor.map((x) => x / 255) as [ | |
number, | |
number, | |
number, | |
number | |
], | |
}; | |
model.shaderInputs.setProps({ hexBitmap: hexBitmapProps }); | |
model.draw(this.context.renderPass); | |
} | |
} | |
_getCoordinateUniforms() { | |
const { hexCorners } = this.props; | |
if (!hexCorners || hexCorners.length !== 6) { | |
return { | |
coordinateConversion: 0, | |
hexCenter: [0, 0] as [number, number], | |
hexRadius: 1, | |
hexCorners: [] as [number, number][], | |
}; | |
} | |
// Altıgen merkezini hesapla | |
let centerX = 0, | |
centerY = 0; | |
const corners2D: [number, number][] = []; | |
for (let i = 0; i < 6; i++) { | |
centerX += hexCorners[i][0]; | |
centerY += hexCorners[i][1]; | |
corners2D.push([hexCorners[i][0], hexCorners[i][1]]); | |
} | |
centerX /= 6; | |
centerY /= 6; | |
// Altıgen yarıçapını hesapla (merkez ile en uzak köşe arası mesafe) | |
let maxDistance = 0; | |
for (let i = 0; i < 6; i++) { | |
const dx = hexCorners[i][0] - centerX; | |
const dy = hexCorners[i][1] - centerY; | |
const distance = Math.sqrt(dx * dx + dy * dy); | |
maxDistance = Math.max(maxDistance, distance); | |
} | |
const { DEFAULT } = COORDINATE_SYSTEM; | |
const { _imageCoordinateSystem: imageCoordinateSystem } = this.props; | |
if (imageCoordinateSystem !== DEFAULT) { | |
// Koordinat sistemi dönüşümü gerekirse burada yapılabilir | |
// Şimdilik basit implementasyon | |
} | |
return { | |
coordinateConversion: 0, | |
hexCenter: [centerX, centerY] as [number, number], | |
hexRadius: maxDistance, | |
hexCorners: corners2D, | |
}; | |
} | |
} | |
/** | |
* Decode uv floats from rgb bytes where b contains 4-bit fractions of uv | |
* @param {Color} color | |
* @returns {number[]} uvs | |
*/ | |
function unpackUVsFromRGB(color: Color): [number, number] { | |
const [u, v, fracUV] = color; | |
const vFrac = (fracUV & 0xf0) / 256; | |
const uFrac = (fracUV & 0x0f) / 16; | |
return [(u + uFrac) / 256, (v + vFrac) / 256]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment