Skip to content

Instantly share code, notes, and snippets.

@cihad
Created June 22, 2025 02:19
Show Gist options
  • Save cihad/409fbf48cda895b342e4b98fd0b3a1d7 to your computer and use it in GitHub Desktop.
Save cihad/409fbf48cda895b342e4b98fd0b3a1d7 to your computer and use it in GitHub Desktop.
// 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,
};
}
// 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);
}
}
`;
// 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>;
// 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);
}
`;
/* 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