From ebdb3828b5f105cb27f202862e63d80e38328cc3 Mon Sep 17 00:00:00 2001 From: Nicolas Lebrun Date: Sat, 1 Jun 2024 19:28:25 +0200 Subject: [PATCH] better hyphenation + demo settings storage --- demo/src/field.ts | 1 - demo/src/index.ts | 79 ++++++++++++++++++++++++++------------- demo/src/quotes.ts | 10 ++--- src/writer/blockWriter.ts | 60 ++++++++++++++++++++++------- 4 files changed, 105 insertions(+), 45 deletions(-) diff --git a/demo/src/field.ts b/demo/src/field.ts index 8c49e2c..d02c0ae 100644 --- a/demo/src/field.ts +++ b/demo/src/field.ts @@ -2,7 +2,6 @@ export type OnChange = (name: string, val: boolean | string | number) => void; const trueFalseCheckbox = (name: string, val: boolean, parent: HTMLElement, onchange: OnChange) => { const fieldset = document.createElement('fieldset'); - const field = document.createElement('input'); field.type = 'checkbox'; diff --git a/demo/src/index.ts b/demo/src/index.ts index eb9b79d..56e9be5 100644 --- a/demo/src/index.ts +++ b/demo/src/index.ts @@ -1,26 +1,45 @@ -import { type Char, getGlyphPath, getParagraphPath } from '../../src/index.ts'; +import { + type Char, + Line, + Vec, + getGlyphPath, + getParagraphPath, + getParagraphVector, +} from '../../src/index.ts'; import { inputRange, textarea, button } from './field.ts'; import { togglablePanel } from './panel.ts'; import { name, version } from '../../package.json'; import quotes from './quotes.ts'; +let settings; + const app = document.getElementById('app'), header = document.createElement('header'), [settingsPanel, openSettingsPanel] = togglablePanel(false, '☰', '✖'), namespace = 'http://www.w3.org/2000/svg', svg = document.createElementNS(namespace, 'svg'), - settings = { - Text: quotes[Math.floor(Math.random() * quotes.length)], - 'Letters per line': 30, - 'Letter spacing': 1, - 'Line spacing': 0.92, + pathFromD = (d: string): SVGPathElement => { + const path = document.createElementNS(namespace, 'path'); + path.setAttribute('d', d); + return path; + }, + updateSetting = (propName: any, propValue: any) => { + settings[propName] = propValue; + localStorage.setItem(propName, String(propValue)); + render(); }, + getSetting = (propName: any): string => + localStorage.getItem(propName) !== null ? localStorage.getItem(propName) : '', group = document.createElementNS(namespace, 'g'); -const pathFromD = (d: string): SVGPathElement => { - const path = document.createElementNS(namespace, 'path'); - path.setAttribute('d', d); - return path; +settings = { + // These settings will be store in the local storage if user change them + Text: getSetting('Text') || quotes[Math.floor(Math.random() * quotes.length)], + 'Letters per line': + getSetting('Letters per line') === '' ? 30 : parseInt(getSetting('Letters per line')), + 'Letter spacing': + getSetting('Letter spacing') === '' ? 0.95 : parseFloat(getSetting('Letter spacing')), + 'Line spacing': getSetting('Line spacing') === '' ? 0.92 : parseFloat(getSetting('Line spacing')), }; const init = () => { @@ -42,12 +61,12 @@ const init = () => { header.appendChild(svgLogo); header.appendChild(openSettingsPanel); - textarea('Text', settings.Text, settingsPanel, updateSettings); + textarea('Text', settings.Text, settingsPanel, updateSetting); inputRange( 'Letters per line', settings['Letters per line'], settingsPanel, - updateSettings, + updateSetting, 12, 120, 1 @@ -56,7 +75,7 @@ const init = () => { 'Letter spacing', settings['Letter spacing'], settingsPanel, - updateSettings, + updateSetting, 0.7, 1.4, 0.01 @@ -65,7 +84,7 @@ const init = () => { 'Line spacing', settings['Line spacing'], settingsPanel, - updateSettings, + updateSetting, 0.5, 1.4, 0.01 @@ -108,25 +127,35 @@ const render = () => { width = window.innerWidth - 40; group.textContent = ''; + /** using getParagraphPath */ const textBlock = getParagraphPath(userInput, settings['Letters per line'], 5, width, [ settings['Letter spacing'], settings['Line spacing'], ]); - textBlock.paths.forEach((d: string) => { - const path = document.createElementNS(namespace, 'path'); - path.setAttribute('d', d); - group.appendChild(path); - }); + textBlock.paths.forEach((d: string) => group.appendChild(pathFromD(d))); + /* using getParagraphVector + const textBlock = getParagraphVector(userInput, settings['Letters per line'], 5, width, [ + settings['Letter spacing'], + settings['Line spacing'], + ]); + textBlock.vectors.forEach((g) => + g.forEach((l: Line) => { + const path = document.createElementNS(namespace, 'path'); + path.setAttribute( + 'd', + l.reduce( + (com: string, v: Vec, i: number) => + (com += `${i === 0 ? 'M' : 'L'}${v[0]},${v[1]}${i === l.length - 1 ? '' : ' '}`), + '' + ) + ); + group.appendChild(path); + }) + );*/ //group.setAttribute('stroke-width', width < 800 ? '0.5' : '2'); svg.setAttribute('width', `${width}`); svg.setAttribute('height', `${textBlock.height + 40}`); svg.setAttribute('viewbox', `0 0 ${width} ${height + 40}`); }; - -const updateSettings = (propName: any, propValue: any) => { - settings[propName] = propValue; - render(); -}; - init(); diff --git a/demo/src/quotes.ts b/demo/src/quotes.ts index d835259..892ad5b 100644 --- a/demo/src/quotes.ts +++ b/demo/src/quotes.ts @@ -1,5 +1,5 @@ export default [ - `“Without this faculty of man and beast alike to recognize identities across the variations of difference, to make allowance for changed conditions, and to preserve the framework of a stable world, art could not exist. When we open our eyes under water we recognize objects, shapes, and colors although through an unfamiliar medium. When we first see pictures we see them in an unfamiliar medium. This is more than a mere pun. The two capacities are interrelated. Every time we meet with an unfamiliar type of transposition, there is a brief moment of shock and a period of adjustment-but it is an adjustment for which the mechanism exists in us.”\r\n + `“Without this faculty of man and beast alike to recognize identities across the variations of difference, to make allowance for changed conditions, and to preserve the framework of a stable world, art could not exist. When we open our eyes under water we recognize objects, shapes, and colors although through an unfamiliar medium. When we first see pictures we see them in an unfamiliar medium. This is more than a mere pun. The two capacities are interrelated. Every time we meet with an unfamiliar type of transposition, there is a brief moment of shock and a period of adjustment-but it is an adjustment for which the mechanism exists in us.”\r\n ― E.H. Gombrich, Art and Illusion: A Study in the Psychology of Pictorial Representation`, `“The artist, no less than the writer, needs a vocabulary before he can embark on a "copy" of reality.”\r\n ― E.H. Gombrich, Art and Illusion: A Study in the Psychology of Pictorial Representation`, @@ -10,12 +10,12 @@ export default [ `“There is no reality without interpretation; just as there is no innocent eye, there is no innocent ear.”\r\n ― E.H. Gombrich, Art and Illusion: A Study in the Psychology of Pictorial Representation`, - `The first question we should ask ourselves when looking at a work of art is: – Does it give me the chance to exist in front of it, or, on the contrary, does it deny me as a subject, refusing the consider the Other in its structure? Does the space-time factor suggested or described by this work, together with the laws governing it, tally with my aspirations in real life? Does it criticise what is deemed to be criticisable? Could I live in a space-time structure corresponding to this reality?\r\n - -― N. BOURRIAUD, Relational Aesthetics`, `“When an artist uses a conceptual form of art, it means that all of the planning and decisions are made beforehand and the execution is a perfunctory affair. The result is a theatrical event.”\r\n ― Sol Lewitt`, `“Everything we see hides another thing, we always want to see what is hidden by what we see. There is an interest in that which is hidden and which the visible does not show us. This interest can take the form of a quite intense feeling, a sort of conflict, one might say, between the visible that is hidden and the visible that is present.”\r\n ― René Magritte`, -]; + +`The electronic image is not fixed to any material base and, like our DNA, it has become a code that can circulate to any container that will hold it, defying death as it travels at the speed of light.\r\n +― Bill Viola` +] diff --git a/src/writer/blockWriter.ts b/src/writer/blockWriter.ts index d2dea49..b3f7f1b 100644 --- a/src/writer/blockWriter.ts +++ b/src/writer/blockWriter.ts @@ -22,16 +22,17 @@ const charArray = (text: string, charsPerLine: number, hyphenFrom: number): Arra const paragraphs: Array = text.split(/\r?\n|\r|\n/g); for (let h = 0; h < paragraphs.length; h++) { - // Separate each word of the paragraph + // Split each word of the paragraph const words: Array = paragraphs[h].split(' '); for (let i = 0; i < words.length; i++) { + // Split each letter of word const letters = words[i].split('') as Array, wordEnd = x + letters.length; - // word on multiple line (add hyphen) - if (wordEnd > charsPerLine) { + // Word cut (multiple line) + if (wordEnd >= charsPerLine) { for (let j = 0, l = letters[0]; j < letters.length; j++, l = letters[j]) { - // word close the line + // word ends at the end of the line if (x === charsPerLine - 1 && i === letters.length - 1) { grid[y].push(l); y++; @@ -39,16 +40,17 @@ const charArray = (text: string, charsPerLine: number, hyphenFrom: number): Arra grid.push([]); } // the next letter is a punctuation mark - else if (x + 1 == charsPerLine && j < letters.length - 1 && isPuncChar(letters[j + 1])) { + else if (x + 2 > charsPerLine && j < letters.length - 1 && isPuncChar(letters[j + 1])) { grid[y].push(...[l, letters[j + 1]]); j++; y++; x = 0; grid.push([]); } - // end line (create new line) + // soft hyphenation between vowel and consonant (create new line) + // test it before applying hard hyphenation to word else if ( - x + 4 > charsPerLine && + x + hyphenFrom > charsPerLine && j < letters.length - 1 && (!isConsonant(l) || isConsonant(letters[j + 1])) ) { @@ -57,8 +59,14 @@ const charArray = (text: string, charsPerLine: number, hyphenFrom: number): Arra x = 0; grid.push([]); } - // before the line end - else { + // hard hyphenation (no rules just prevent line larger than the limit) + else if (x + 1 > charsPerLine) { + grid[y].push(...[l as Char, '-' as Char]); + y++; + x = 0; + grid.push([]); + } else if (x + 1 < charsPerLine) { + /* before the line end */ grid[y].push(l); if (j == letters.length - 1) { grid[y].push(' ' as Char); @@ -66,6 +74,15 @@ const charArray = (text: string, charsPerLine: number, hyphenFrom: number): Arra } x++; } + // put the entire word on the next line + else { + const erase = letters.length - j; + grid[y] = grid[y].splice(0, grid[y].length - erase); + y++; + grid[y] = [...[...letters, ' ' as Char]]; + x = letters.length; + break; + } } } // word close the line (add space to close the line) @@ -77,7 +94,7 @@ const charArray = (text: string, charsPerLine: number, hyphenFrom: number): Arra x = 0; } // word on same line and sufficient room (add space after word) - else if (wordEnd < charsPerLine - hyphenFrom) { + else if (wordEnd < charsPerLine) { grid[y].push(...[...letters, ' ' as Char]); x += letters.length + 1; } @@ -106,19 +123,25 @@ const getParagraphVector = ( text: string, charsPerLine: number, hyphenFrom: number, - textWidth: number -): Glyph[] => { + textWidth: number, + spacing = [1, 1] +): { vectors: Glyph[]; height: number } => { const grid = charArray(text, charsPerLine, hyphenFrom); const textSize = [textWidth / charsPerLine, (textWidth / charsPerLine) * 1.4]; - return grid.reduce( + const invSpacing = [(1 / spacing[0]) * textSize[0], (1 / spacing[1]) * textSize[1]]; + const vectors = grid.reduce( (out: Glyph[], row: Array, y: number) => [ ...out, ...row.map((l: Char, x: number) => - getGlyphVector(l, textSize, [x * textSize[0], y * textSize[1]]) + getGlyphVector(l, invSpacing, [x * textSize[0], y * textSize[1]]) ), ], [] ); + return { + vectors, + height: grid.length * textSize[1], + }; }; /** @@ -157,6 +180,15 @@ const getParagraphPath = ( ] as string[], [] as string[] ); + + /** debug charsPerLine + console.log( + `Line > ${charsPerLine}`, + grid + .filter((lin) => lin.length > charsPerLine) + .map((len)=> grid.indexOf(len)) + ) + */ return { paths, height: textSize[1] * grid.length }; };