scrawl-canvas
Version:
Version 8.9.4 - 19 Nov 2022
366 lines (301 loc) • 13.3 kB
JavaScript
// # Demo Snippets 006
// Editable header text colorizer and animation effect snippets
//
// Related files:
// + [Editable header text colorizer and animation effect snippets](../snippets-006.html)
// + [Animated highlight gradient text snippet](./animated-highlight-gradient-text-snippet.html)
// + [Bubbles text snippet](./bubbles-text-snippet.html)
// + [Risograph text gradient snippet](./risograph-text-gradient-snippet.html)
// + [Swirling stripes text snippet](./swirling-stripes-text-snippet.html)
// + [Worley text gradient snippet](./worley-text-gradient-snippet.html)
// ### Text snippet helper function
//
// __Purpose__ - Used by text snippets to handle some boilerplate and common functionality:
// + Handles most work related to matching the canvas font to the HTML element's font
// + Retrieves the `data-` attributes which handle canvas text alignment with the HTML element's text layout
// + For editable HTML elements, handles user updates to the text
// + Canvas initialization, including accessibility
// + Phrase entity initialisation, and updates - including adapting the HTML element to accommodate the phrase's dimensions
// + Animation boilerplate code, including animation accessibility - create and manage the `play|pause` `button` element created and added to the HTML element
//
// __Function input__ - The Scrawl-canvas snippet object, alongside the SC object (both arguments required).
//
// __Function output__ - A Javascript object with the following `key:value` attributes:
// + `canvas` - SC snippet's Canvas wrapper
// + `group` - (string) Canvas wrapper's base Cell's name value
// + `animation` - SC snippet's animation object
// + `wrapper` - SC snippet's Element artefact wrapper
// + `dataset` - HTML element's `dataset` object
// + `compStyles` - HTML element's `elementComputedStyles` object
// + `name` - (string) SC snippet's Element artefact wrapper's name value
//
// + `width` - (number px) initial HTML element width value
// + `height` - (number px) initial HTML element height value
// + `lineHeight` - (number px) initial HTML element line height value
// + `fontSize` - (number px) initial HTML element font size value
// + `yOffset` - user-defined canvas font vertical positioning correction value
//
// + `textGroup` - the non-cell group used to manage and update the two Phrase entitys
//
// + `initCanvas` - function to invoke to initialize the Canvas wrapper
// + `initPhrase` - function to invoke to initialize the Phrase entitys
// + `processText` - function to handle `contenteditable` element text updates
//
// + `responsiveFunctions` - Array to hold snippet-specific functions relating to responsiveness
// + `animationFunctions` - Array to hold snippet-specific functions relating to running animations
// + `animationStartFunctions` - Array to hold snippet-specific functions relating to `animation.run()`
// + `animationEndFunctions` - Array to hold snippet-specific functions relating to `animation.halt()`
// + `contrastMoreActions` - Array to hold a `prefers-contrast` accessibility change function
// + `contrastOtherActions` - Array to hold a `prefers-contrast` accessibility change function
// + `colorSchemeDarkActions` - Array to hold a `prefers-color-scheme` accessibility change function
// + `colorSchemeLightActions` - Array to hold a `prefers-color-scheme` accessibility change function
// + `eternalTweens` - Array to hold sets of continuously-running tweens created by the snippet
// + `additionalDemolishActions` - Array to hold snippet-specific functions relating to snippet demolition
export const getSnippetData = (snippet, scrawl) => {
const canvas = snippet.canvas,
group = canvas.base.name,
animation = snippet.animation,
wrapper = snippet.element,
demolishAction = snippet.demolish,
el = wrapper.domElement,
compStyles = wrapper.elementComputedStyles,
name = wrapper.name,
dataset = el.dataset;
const {fontStyle, fontVariant, fontWeight, fontSize, fontFamily, lineHeight, color, width, height, backgroundColor, textAlign, letterSpacing} = compStyles;
let yOffset = 0,
lineheightAdjuster = 1,
underlinePosition = 0.8,
underlineWidth = 0.05,
noUnderlineGlyphs = '',
isAnimated = false;
if (dataset.yOffset) yOffset = parseFloat(dataset.yOffset);
else {
const s = compStyles.getPropertyValue('--data-y-offset');
if (s) yOffset = parseFloat(s);
}
if (dataset.lineheightAdjuster) lineheightAdjuster = parseFloat(dataset.lineheightAdjuster);
else {
const s = compStyles.getPropertyValue('--data-lineheight-adjuster');
if (s) lineheightAdjuster = parseFloat(s);
}
if (dataset.underlinePosition) underlinePosition = parseFloat(dataset.underlinePosition);
else {
const s = compStyles.getPropertyValue('--data-underline-position');
if (s) underlinePosition = parseFloat(s);
}
if (dataset.underlineWidth) underlineWidth = parseFloat(dataset.underlineWidth);
else {
const s = compStyles.getPropertyValue('--data-underline-width');
if (s) underlineWidth = parseFloat(s);
}
if (dataset.noUnderlineGlyphs) noUnderlineGlyphs = dataset.noUnderlineGlyphs;
else {
const s = compStyles.getPropertyValue('--data-no-underline-glyphs');
if (s) noUnderlineGlyphs = s;
}
if (dataset.isAnimated) isAnimated = dataset.isAnimated;
else {
const s = compStyles.getPropertyValue('--data-is-animated');
if (s) isAnimated = s;
}
const additionalDemolishActions = [];
const responsiveFunctions = [];
const animationFunctions = [];
const animationStartFunctions = [];
const animationEndFunctions = [];
const contrastMoreActions = [];
const contrastOtherActions = [];
const colorSchemeDarkActions = [];
const colorSchemeLightActions = [];
const eternalTweens = [];
const initCanvas = () => {
el.style.color = 'transparent';
el.style.overflow = 'hidden';
canvas.setColorSchemeDarkAction(() => colorSchemeDarkActions.forEach(a => a(compStyles)));
canvas.setColorSchemeLightAction(() => colorSchemeLightActions.forEach(a => a(compStyles)));
canvas.setMoreContrastAction(() => contrastMoreActions.forEach(a => a(compStyles)));
canvas.setOtherContrastAction(() => contrastOtherActions.forEach(a => a(compStyles)));
};
const addTextNode = () => {
const shy = document.createTextNode('!');
el.appendChild(shy);
};
const processText = t => {
t = t.replace(/<canvas.*<\/canvas>/gi, '');
t = t.replace(/<button.*<\/button>/gi, '');
if (!t.length) {
addTextNode();
t = '!';
}
return t;
}
if (el.getAttribute('contenteditable')) {
const updateText = (e) => {
textGroup.setArtefacts({ text: processText(el.innerHTML) });
}
const focusText = (e) => {
el.style.color = 'gray';
}
const blurText = (e) => {
el.style.color = 'transparent';
}
scrawl.addNativeListener('input', updateText, el);
scrawl.addNativeListener('focus', focusText, el);
scrawl.addNativeListener('blur', blurText, el);
additionalDemolishActions.push(() => {
scrawl.removeNativeListener('input', updateText, el);
scrawl.removeNativeListener('focus', focusText, el);
scrawl.removeNativeListener('blur', blurText, el);
});
}
const initPhrase = (phrase) => {
const localLineHeight = parseFloat(lineHeight),
localFontSize = parseFloat(fontSize);
phrase.set({
style: fontStyle,
variant: fontVariant,
weight: fontWeight,
size: fontSize,
family: fontFamily,
lineHeight: (localLineHeight / localFontSize) * lineheightAdjuster,
width: '100%',
text: processText(el.innerHTML),
startY: Math.round(localFontSize * yOffset),
underlinePosition,
underlineWidth: Math.round(localFontSize * underlineWidth) || 1,
noUnderlineGlyphs,
underlineStyle: 'black',
justify: textAlign || 'left',
letterSpacing: letterSpacing || 0,
exposeText: false,
});
};
// This is a specific fix to handle cases where, because of differences in the way SC and the browser calculate how to fit words on a line, SC phrases may break the text onto an additional line not required by the DOM element
let localWidth = width;
let localHeight = parseFloat(height);
const textHeightAction = () => {
if (textGroup.artefacts.length) {
const art = textGroup.getArtefact(textGroup.artefacts[0]);
const headerHeight = art.get('height');
if (headerHeight !== localHeight) {
el.style.height = `${headerHeight}px`;
localHeight = headerHeight;
}
}
};
const updatePhrase = () => {
if (textGroup.artefacts.length && compStyles.width !== localWidth) {
localWidth = compStyles.width;
const {fontStyle, fontVariant, fontWeight, fontSize, fontFamily, lineHeight, textAlign, letterSpacing} = compStyles;
const localLineHeight = parseFloat(lineHeight),
localFontSize = parseFloat(fontSize);
textGroup.setArtefacts({
style: fontStyle,
variant: fontVariant,
weight: fontWeight,
size: fontSize,
family: fontFamily,
lineHeight: (localLineHeight / localFontSize) * lineheightAdjuster,
startY: Math.round(localFontSize * yOffset),
underlineWidth: Math.round(localFontSize * underlineWidth) || 1,
justify: textAlign || 'left',
letterSpacing: letterSpacing || 0,
});
responsiveFunctions.forEach(r => r(compStyles));
}
};
const textGroup = scrawl.makeGroup({
name: `${name}-text-group`,
});
let doAnimation = false;
const commenceActions = () => {
updatePhrase();
textHeightAction();
if (doAnimation) {
animationFunctions.forEach(f => f(compStyles));
}
}
animation.set({
commence: commenceActions,
});
if (isAnimated) {
doAnimation = true;
if ('static' === compStyles.position) el.style.position = 'relative';
const control = document.createElement('button');
control.style.position = 'absolute';
control.style.fontSize = '12px';
control.style.display = 'block';
control.style.top = '0';
control.style.right = '0';
control.textContent = 'Halt';
control.setAttribute('contenteditable', 'false');
const controlClick = (e) => {
doAnimation = !doAnimation;
if (doAnimation) {
control.textContent = 'Halt';
animationStartFunctions.forEach(a => a());
eternalTweens.forEach(t => t.resume());
}
else {
control.textContent = 'Play';
animationEndFunctions.forEach(a => a());
eternalTweens.forEach(t => t.halt());
}
};
scrawl.addNativeListener('click', controlClick, control);
el.appendChild(control);
canvas.setReduceMotionAction(() => {
if (doAnimation) {
doAnimation = false;
control.textContent = 'Play';
animationEndFunctions.forEach(a => a());
eternalTweens.forEach(t => t.halt());
}
});
canvas.setNoPreferenceMotionAction(() => {
if (!doAnimation) {
doAnimation = true;
control.textContent = 'Halt';
animationStartFunctions.forEach(a => a());
eternalTweens.forEach(t => t.resume());
}
});
additionalDemolishActions.push(() => {
scrawl.removeNativeListener('click', controlClick, control);
});
}
snippet.demolish = () => {
eternalTweens.forEach(t => t.kill());
textGroup.kill();
additionalDemolishActions.forEach(f => f());
demolishAction();
};
return {
canvas,
group,
animation,
wrapper,
dataset,
compStyles,
name,
width: parseFloat(width),
height: localHeight,
lineHeight: parseFloat(lineHeight),
fontSize: parseFloat(fontSize),
yOffset,
textGroup,
initCanvas,
initPhrase,
processText,
responsiveFunctions,
animationFunctions,
animationStartFunctions,
animationEndFunctions,
contrastMoreActions,
contrastOtherActions,
colorSchemeDarkActions,
colorSchemeLightActions,
eternalTweens,
additionalDemolishActions,
};
}