Skip to content
Open
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
6 changes: 6 additions & 0 deletions crates/koharu-renderer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ use specta::Type;
#[serde(default)]
pub struct TypesettingConfig {
pub font_families: Vec<String>,
pub force_border_width: Option<f32>,
pub force_black_text: bool,
pub force_font_weight: Option<u16>,
}

impl Default for TypesettingConfig {
fn default() -> Self {
Self {
font_families: vec!["CCWildWords".to_owned(), "Adobe 黑体 Std".to_owned()],
force_border_width: None,
force_black_text: false,
force_font_weight: None,
}
}
}
Expand Down
41 changes: 35 additions & 6 deletions crates/koharu-renderer/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,9 @@ impl Renderer {
height,
source_role: &source_role,
font_families: &typesetting.font_families,
force_border_width: typesetting.force_border_width,
force_black_text: typesetting.force_black_text,
force_font_weight: typesetting.force_font_weight,
balloon_flows: flow_plan.placements,
layers: Vec::new(),
dependencies: BTreeSet::from([
Expand Down Expand Up @@ -589,6 +592,9 @@ struct Traversal<'a> {
height: u32,
source_role: &'a AssetRole,
font_families: &'a [String],
force_border_width: Option<f32>,
force_black_text: bool,
force_font_weight: Option<u16>,
balloon_flows: HashMap<EntityId, ResolvedPlacement>,
layers: Vec<LayerDraft>,
dependencies: BTreeSet<RenderDependency>,
Expand Down Expand Up @@ -816,6 +822,32 @@ impl Traversal<'_> {
dependencies.insert(RenderDependency::Font(family.clone()));
}
let is_bubble = balloon_contour.is_some();
let mut foreground_color = typography
.as_ref()
.and_then(|value| value.color)
.unwrap_or([0, 0, 0, 255]);

if self.force_black_text {
foreground_color = [0, 0, 0, 255];
}
let mut stroke = resolve_stroke(typography.as_ref());
if let Some(forced_width) = self.force_border_width {
if let Some(ref mut existing_stroke) = stroke {
existing_stroke.width_px = forced_width;
} else if forced_width > 0.0 {
stroke = Some(StrokeOptions {
color: typography
.as_ref()
.and_then(|t| t.stroke_color)
.unwrap_or([255, 255, 255, 255]),
width_px: forced_width,
});
}
}
let mut resolved_weight = typography.as_ref().and_then(|value| value.font_weight);
if let Some(fw) = self.force_font_weight {
resolved_weight = Some(fw);
}
let descriptor = TextNodeDescriptor {
entity,
text: text.clone(),
Expand All @@ -826,7 +858,7 @@ impl Traversal<'_> {
flow_contour,
preferred_font,
font_families,
font_weight: typography.as_ref().and_then(|value| value.font_weight),
font_weight: resolved_weight,
font_style: typography
.as_ref()
.and_then(|value| value.font_style)
Expand All @@ -836,11 +868,8 @@ impl Traversal<'_> {
auto_fit: typography.as_ref().is_none_or(|value| value.auto_fit),
alignment,
writing_mode,
foreground_color: typography
.as_ref()
.and_then(|value| value.color)
.unwrap_or([0, 0, 0, 255]),
stroke: resolve_stroke(typography.as_ref()),
foreground_color,
stroke,
line_height: 1.2,
letter_spacing: 0.0,
word_spacing: 0.0,
Expand Down
9 changes: 6 additions & 3 deletions packages/bridge/src/protocol.ts

Large diffs are not rendered by default.

57 changes: 44 additions & 13 deletions packages/koharu/components/editor/Inspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,19 @@ function TypeInspector() {
const page = usePage().data
const selectedIds = useKoharuStore((state) => state.selectedLayers)
const availableFonts = useFonts().data

const storeState = useKoharuStore((state: any) => state)
const config = storeState.config || storeState.preferences
const typesetting = config?.typesetting

const forcedBlackText = typesetting?.force_black_text
const forcedBorderWidth = typesetting?.force_border_width
const forcedFontWeight = typesetting?.force_font_weight

const isColorForced = !!forcedBlackText
const isBorderForced = forcedBorderWidth !== undefined && forcedBorderWidth !== null
const isWeightForced = forcedFontWeight !== undefined && forcedFontWeight !== null

const expandedSelection = page ? expandLayerSelection(page.layers, selectedIds) : []
const selected =
page?.layers.filter(isTextLayer).filter((layer) => expandedSelection.includes(layer.id)) ?? []
Expand Down Expand Up @@ -218,12 +231,12 @@ function TypeInspector() {
}}
/>
</InspectorField>
<InspectorField label={t('inspector.color')}>
<InspectorField label={t('inspector.color')} forced={isColorForced}>
<ColorWell
label={t('inspector.textColor')}
size='sm'
disabled={disabled}
value={rgbaToHex(typography.color ?? defaultTypography.color!)}
disabled={disabled || isColorForced}
value={isColorForced ? '#000000' : rgbaToHex(typography.color ?? defaultTypography.color!)}
onChange={(color) => apply((value) => ({ ...value, color: hexToRgba(color) }))}
/>
</InspectorField>
Expand All @@ -245,10 +258,10 @@ function TypeInspector() {
}
/>
</InspectorField>
<InspectorField label={t('inspector.weight')}>
<InspectorField label={t('inspector.weight')} forced={isWeightForced}>
<Select
disabled={disabled}
value={String(weight)}
disabled={disabled || isWeightForced}
value={isWeightForced ? String(forcedFontWeight) : String(weight)}
onValueChange={(font_weight) =>
apply((value) => ({
...value,
Expand Down Expand Up @@ -401,13 +414,13 @@ function TypeInspector() {
}}
/>
</InspectorField>
<InspectorField label={t('inspector.width')}>
<InspectorField label={t('inspector.width')} forced={isBorderForced}>
<NumberField
id={borderWidthId}
name='border-width'
className='min-w-0'
disabled={disabled}
value={displayedStrokeWidth}
disabled={disabled || isBorderForced}
value={isBorderForced ? forcedBorderWidth : displayedStrokeWidth}
min={0.5}
max={32}
step={0.5}
Expand Down Expand Up @@ -878,12 +891,30 @@ function LayerEditor({ layer, onDelete }: { layer: Layer; onDelete?: () => void
)
}

function InspectorField({ label, children }: { label: string; children: React.ReactNode }) {
function InspectorField({
label,
forced,
children,
}: {
label: string
forced?: boolean
children: React.ReactNode
}) {
return (
<div className='grid min-w-0 gap-0.5'>
<span className='text-[8px] font-medium tracking-[0.06em] text-muted-foreground uppercase'>
{label}
</span>
<div className='flex items-center gap-1.5'>
<span className='truncate text-[8px] font-medium tracking-[0.06em] text-muted-foreground uppercase'>
{label}
</span>
{forced && (
<div
className='relative flex size-2.5 shrink-0 items-center justify-center overflow-hidden rounded-[1px] border border-muted-foreground/40 bg-transparent'
title='Overridden by global setting'
>
<div className='absolute h-[0.5px] w-[120%] -rotate-45 bg-muted-foreground/40' />
</div>
)}
</div>
{children}
</div>
)
Expand Down
121 changes: 120 additions & 1 deletion packages/koharu/components/preferences/TypesettingPreferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'

import { FontPicker } from '@/components/controls/FontPicker'
import { PreferencePage } from '@/components/preferences/PreferenceFields'
import {
PreferencePage,
PreferenceRow,
PreferenceSection,
} from '@/components/preferences/PreferenceFields'
import { useFonts } from '@/lib/queries'
import type { TypesettingConfig } from '@koharu/bridge/protocol'
import { Button } from '@koharu/ui/components/button'
Expand Down Expand Up @@ -133,6 +137,121 @@ export function TypesettingPreferences({
</p>
)}
</section>

{}
<PreferenceSection
title={t('settings.typesetting.overrides.title')}
description={t('settings.typesetting.overrides.description')}
>
<PreferenceRow
title={t('settings.typesetting.overrides.fontColor')}
description={t('settings.typesetting.overrides.fontColorDescription')}
>
<Button
type='button'
variant={value.force_black_text ? 'default' : 'outline'}
className='h-8 text-[11px]'
onClick={() =>
onChange({
...value,
force_black_text: !value.force_black_text,
})
}
>
{value.force_black_text ? t('settings.typesetting.overrides.revertAuto') : t('settings.typesetting.overrides.forceBlack')}
</Button>
</PreferenceRow>

<PreferenceRow
title={t('settings.typesetting.overrides.borderWidth')}
description={t('settings.typesetting.overrides.borderWidthDescription')}
>
<div className='flex items-center gap-2'>
{value.force_border_width !== null && value.force_border_width !== undefined && (
<div className='flex items-center gap-1.5'>
<input
type='number'
min='0'
step='0.1'
className='h-8 w-16 rounded-md border border-input bg-transparent px-2 py-1 text-right text-[11px] tabular-nums shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'
value={value.force_border_width}
onChange={(e) => {
const num = parseFloat(e.target.value)
onChange({ ...value, force_border_width: isNaN(num) ? 0 : num })
}}
/>
<span className='select-none text-[11px] text-muted-foreground'>px</span>
</div>
)}
<Button
type='button'
variant={
value.force_border_width !== null && value.force_border_width !== undefined
? 'default'
: 'outline'
}
className='h-8 text-[11px]'
onClick={() =>
onChange({
...value,
force_border_width:
value.force_border_width !== null && value.force_border_width !== undefined
? null
: 0.5,
})
}
>
{value.force_border_width !== null && value.force_border_width !== undefined
? t('settings.typesetting.overrides.revert')
: t('settings.typesetting.overrides.override')}
</Button>
</div>
</PreferenceRow>

<PreferenceRow
title={t('settings.typesetting.overrides.fontWeight')}
description={t('settings.typesetting.overrides.fontWeightDescription')}
>
<div className='flex items-center gap-2'>
{value.force_font_weight !== null && value.force_font_weight !== undefined && (
<input
type='number'
min='100'
max='900'
step='100'
className='h-8 w-16 rounded-md border border-input bg-transparent px-2 py-1 text-right text-[11px] tabular-nums shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'
value={value.force_font_weight}
onChange={(e) => {
const num = parseInt(e.target.value, 10)
onChange({ ...value, force_font_weight: isNaN(num) ? 400 : num })
}}
/>
)}
<Button
type='button'
variant={
value.force_font_weight !== null && value.force_font_weight !== undefined
? 'default'
: 'outline'
}
className='h-8 text-[11px]'
onClick={() =>
onChange({
...value,
force_font_weight:
value.force_font_weight !== null && value.force_font_weight !== undefined
? null
: 400,
})
}
>
{value.force_font_weight !== null && value.force_font_weight !== undefined
? t('settings.typesetting.overrides.revert')
: t('settings.typesetting.overrides.override')}
</Button>
</div>
</PreferenceRow>
</PreferenceSection>
</PreferencePage>
)
}
Expand Down
18 changes: 17 additions & 1 deletion packages/koharu/public/locales/en-US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,23 @@
"loadError": "Fonts could not be loaded. Existing families can still be removed.",
"loadingFonts": "Loading fonts…",
"removeFamily": "Remove {{family}}",
"title": "Typesetting"
"title": "Typesetting",

"overrides": {
"title": "Global Overrides",
"description": "Force specific typography settings across all text layers.",
"fontColor": "Font Color",
"fontColorDescription": "Override the typography color with solid black.",
"borderWidth": "Font Border Width",
"borderWidthDescription": "Override the automatically detected stroke width.",
"fontWeight": "Font Weight",
"fontWeightDescription": "Override the font thickness globally.",
"forceBlack": "Force Black",
"revertAuto": "Revert to Auto",
"override": "Override",
"revert": "Revert"
}

}
},
"start": {
Expand Down
17 changes: 16 additions & 1 deletion packages/koharu/public/locales/es-ES/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,22 @@
"loadError": "No se pudieron cargar las fuentes. Las familias existentes aún se pueden eliminar.",
"loadingFonts": "Cargando fuentes…",
"removeFamily": "Eliminar {{family}}",
"title": "Composición tipográfica"
"title": "Composición tipográfica",

"overrides": {
"title": "Global Overrides",
"description": "Force specific typography settings across all text layers.",
"fontColor": "Font Color",
"fontColorDescription": "Override the typography color with solid black.",
"borderWidth": "Font Border Width",
"borderWidthDescription": "Override the automatically detected stroke width.",
"fontWeight": "Font Weight",
"fontWeightDescription": "Override the font thickness globally.",
"forceBlack": "Force Black",
"revertAuto": "Revert to Auto",
"override": "Override",
"revert": "Revert"
}
}
},
"start": {
Expand Down
Loading