Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: #5404 cjk keep up right bugfix (suggestion) - draft version #5405

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/assets/examples/cjk-keep-up-right.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions src/symbol/shaping.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {
charHasUprightVerticalOrientation,
charAllowsIdeographicBreaking,
charInComplexShapingScript
charInComplexShapingScript,
shouldTreatAsUpright
} from '../util/script_detection';
import {verticalizePunctuation} from '../util/verticalize_punctuation';
import {rtlWorkerPlugin} from '../source/rtl_text_plugin_worker';
Expand Down Expand Up @@ -644,7 +645,7 @@ function shapeLines(shaping: Shaping,
let verticalAdvance = ONE_EM;
const vertical = !(writingMode === WritingMode.horizontal ||
// Don't verticalize glyphs that have no upright orientation if vertical placement is disabled.
(!allowVerticalPlacement && !charHasUprightVerticalOrientation(codePoint)) ||
(!allowVerticalPlacement && (!charHasUprightVerticalOrientation(codePoint) && !shouldTreatAsUpright(line.toString(), i))) ||
// If vertical placement is enabled, don't verticalize glyphs that
// are from complex text layout script, or whitespaces.
(allowVerticalPlacement && (whitespace[codePoint] || charInComplexShapingScript(codePoint))));
Expand Down
25 changes: 25 additions & 0 deletions src/util/script_detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,28 @@ export function isStringInSupportedScript(chars: string, canRenderRTL: boolean)
}
return true;
}

/**
* Returns true if the character at given index should be treated as upright in vertical text
* This helps handle numbers between CJK characters consistently
*/
export function shouldTreatAsUpright(text: string, index: number): boolean {
const char = text.charCodeAt(index);

// If it's a CJK character
if (charHasUprightVerticalOrientation(char)) {
return true;
}

// If the character is a number
if (char >= 0x30 && char <= 0x39) {
// Check if there are any CJK characters in the entire text
for (let i = 0; i < text.length; i++) {
if (i !== index && charHasUprightVerticalOrientation(text.charCodeAt(i))) {
return true; // If CJK characters exist, treat numbers as upright too
}
}
}

return false;
}
159 changes: 159 additions & 0 deletions test/examples/cjk-keep-up-right.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<!DOCTYPE html>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be better as a render test instead of an example.

<html lang="en">
<head>
<title>MapLibre GL JS CJK Keep Upright Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel='stylesheet' href='../../dist/maplibre-gl.css' />
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script src='../../dist/maplibre-gl-dev.js'></script>
<script>
const languages = [
{ id: 'korean', text: '반포대로21길' },
{ id: 'japanese', text: '銀座通り21号' },
{ id: 'chinese', text: '南京路21号' },
{ id: 'english', text: 'Street 123' },
];

const config = {
startX: 127.0246,
xSpacing: 0.005,
baseY: {
line: 37.5000,
center: 37.4960,
curve: 37.4920
},
radius: 0.002
};

const centers = languages.map((lang, index) => ({
...lang,
x: config.startX + (config.xSpacing * index)
}));

const mapCenter = [
(centers[0].x + centers[centers.length - 1].x) / 2,
(config.baseY.line + config.baseY.curve) / 2
];

const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: mapCenter,
zoom: 15,
hash: true,
localIdeographFontFamily: "'Noto Sans', 'Noto Sans CJK SC', 'Noto Sans Thai', 'Noto Sans Devanagari', sans-serif"
});

map.on('load', () => {
function createRadialFeatures(centerX, baseY, text) {
const angles = Array.from({length: 8}, (_, i) => (i * Math.PI / 4));
return angles.map(angle => ({
'type': 'Feature',
'geometry': {
'type': 'LineString',
'coordinates': [
[centerX, baseY],
[
centerX + config.radius * Math.cos(angle),
baseY + config.radius * Math.sin(angle)
]
]
},
'properties': { 'name': text }
}));
}

function smoothAmplitude(t) {
return Math.sin(t * Math.PI) * 0.8;
}

function createSCurve(centerX, text) {
const points = [];
const segments = 32;

for (let i = 0; i <= segments; i++) {
const t = i / segments;
const x = centerX + config.radius * (2 * t - 1);
const amplitude = smoothAmplitude(t);
const y = config.baseY.curve + config.radius * amplitude * Math.sin(t * Math.PI * 4);
points.push([x, y]);
}
return {
'type': 'Feature',
'geometry': {
'type': 'LineString',
'coordinates': points
},
'properties': { 'name': text }
};
}

function createLabelLayer(id, source, placement) {
const layout = {
'text-field': ['get', 'name'],
'symbol-placement': placement,
'text-size': 12,
'text-keep-upright': true,
'text-allow-overlap': true,
'text-ignore-placement': true,
'text-anchor': 'center',
'symbol-spacing': 5,
};

return {
'id': `test-labels-${id}`,
'type': 'symbol',
'source': source,
'layout': layout,
'paint': {
'text-color': '#000000',
'text-halo-color': '#ffffff',
'text-halo-width': 2
}
};
}

const features = {
line: centers.flatMap(c => createRadialFeatures(c.x, config.baseY.line, c.text)),
center: centers.flatMap(c => createRadialFeatures(c.x, config.baseY.center, c.text)),
curve: centers.map(c => createSCurve(c.x, c.text))
};

Object.entries(features).forEach(([type, typeFeatures]) => {
const sourceId = `test-labels-${type}`;

map.addSource(sourceId, {
'type': 'geojson',
'data': {
'type': 'FeatureCollection',
'features': typeFeatures
}
});

map.addLayer({
'id': `test-lines-${type}`,
'type': 'line',
'source': sourceId,
'paint': {
'line-color': '#888',
'line-width': 2
}
});

map.addLayer(createLabelLayer(
type,
sourceId,
type === 'center' ? 'line-center' : 'line'
));
});
});
</script>
</body>
</html>