When I use two different fonts in a web page at the same font-size, they often display at different actual sizes:
This example uses two Google Fonts, Gentium and Metamorphous at the same font-size, specified as 20px.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<link id="Gentium Book Basic" rel="stylesheet" type="text/css"
href="http://fonts.googleapis.com/css?family=Gentium Book Basic"
media="all">
<link id="Metamorphous" rel="stylesheet" type="text/css"
href="http://fonts.googleapis.com/css?family=Metamorphous" media="all">
</head>
<body style="font-size: 20px">
<span style="font-family: Gentium Book Basic">Test Text Length (Gentium)</span>
<br>
<span style="font-family: Metamorphous">Test Text Length (Metamorphous) </span>
</body>
</html>
A JSBin for this example can be found here.
My understanding of specifying font-size in an absolute length like px was that the font would be scaled to match that length. My expectation is that two different fonts at the same font-size would have either matching height or matching length (I understand the aspect ratios of the fonts may be different). But it doesn't appear that either is the case here. Is there some way I can make two arbitrary fonts display at either the same height or the same length without manually calculating and applying a correction?
EDIT: An example showing the descender to ascender distance for two fonts displayed at the same font size.
Clearly the two distances are not the same for these two fonts as displayed.
EDIT: An example showing letters with and without accents in the two fonts:
Again, clearly the letters are different sizes.
EDIT: Going on what is described in this article, the issue is that font-size controls the displayed size of the em value of the font. But the em value is arbitrary (it doesn't have to correspond to anything within the font, and in particular is not necessarily the height of a lower case 'm'), and does not include the ascenders and descenders, which can be any size at all (example taken from above article):
so the result is that a "100px" font can be just about any effective size whatsoever. The author of the above article computed the range of effective sizes for the Google Web Fonts at the time to be 0.618 to 3.378.
Since the font metrics (such as the em size, the capitals height, the ascender and descender values) are not exposed in CSS, there doesn't seem to be any way within CSS to make two arbitrary fonts the same effective size. For any particular font, you can use a font editor to find the font metric values and use those numbers to scale the font as required. For an arbitrary font, an option is to display some text and use the measured bounding box to determine the effective size and calculate an appropriate scaling factor.
My thanks to everyone who contributed to explaining this!
I spent a lot of time crawling through StackOverflow looking for answers for a similar situation and ended up not finding anything perfect. What I ended up doing is measuring the two fonts, and then adjusting the top margin and scale of the second font to match the first. (By using scale instead of changing the font size, it allows us to not need to re-calculate the text metrics after resizing)
I put together a couple of pens for posterity. Here's the second one, which handles the normalization of font sizes and alignments between two fonts: https://codepen.io/zacholas/pen/oNBPWga
And the first one, that only handles the measuring, is: https://codepen.io/zacholas/pen/ExZwJjx
I guess I have to paste some code in order to link to codepen, so here's all the comparison code:
HTML:
<h1>Welcome</h1>
<p>
<strong>What's all this about?</strong><br>
I've been working on the new version of the Mason image editor app, and in it, I need to compare multiple fonts to replace them with each other to have the final layout not look crappy due to spacing differences between fonts. (To allow users to customize fonts in templates and have them still look nice)
</p>
<p>The first pen focused on getting all the measurements and ratios necessary.</p>
<p>This pen encompasses the second part, which is comparison and normalization of different fonts against a default.</p>
<p>
<strong>How it works</strong><br>
First we get the metrics for the two fonts and compare their top/bottom spacing. We then normalize them to align in a purdy vertically-centered way. And then we scale down the second font's container so that it matches the size of the first.
</p>
<p>Enjoy!</p>
<p><em>- Zach</em></p>
<hr>
<h3>Demo</h3>
<p>If all of my code is working correctly, the text in the "new font adjusted" box should look all purdy and be vertically and horizontally centered.</p>
<p><strong>NOTE:</strong><em> You'll need to make a change in the dropdown before the text in the "new font adjusted" box actually gets adjusted.</em></p>
<label for="font-picker">Choose a font to swap:</label>
<select id="font-picker" disabled>
<option value=""> — Template Default — </option>
</select>
<div >
<div id="image-box" class="flex-row">
<div>
<h6>Original:</h6>
<div class="reference-box">
<div class="text-background">
<div class="text-container text-utc-lander" id="original-text">
Hxy
</div>
</div>
</div>
</div>
<div>
<h6>New font unadjusted:</h6>
<div class="reference-box">
<div class="text-background">
<div class="text-container text-utc-lander" id="unadjusted-text">
Hxy
</div>
</div>
</div>
</div>
<div>
<h6>New font adjusted:</h6>
<div id="modified" class="reference-box">
<div class="text-background">
<div class="scaler" id="adjusted-text-scaler">
<div class="text-container text-utc-lander" id="adjusted-text">
Hxy
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<hr>
<h2>Canvases used for calculating</h2>
<div id="sample-output-container">
</div>
SCSS:
#import 'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css';
// The font size in the demo boxes
$test-font-size: 200px!default;
// $test-font-size: 100px;
body {
background: #eee;
padding: 10px;
font-family: Arial;
}
hr {
margin: 40px 0;
}
h6 {
font-size: 17px;
margin: 12px 0 5px 0;
}
//* In production, you should probably use code like this to position canvases off-screen:
// .test-canvas {
// position: fixed;
// top: -99999px;
// left: -99999px;
// display:none;
// }
.text-utc-lander { font-family: 'UTCLander-Regular'; }
.text-ar-bonnie { font-family: 'ARBONNIE'; }
.text-adam-cg { font-family: 'ADAMCGPRO'; }
.text-abolition { font-family: 'Abolition-Regular'; }
.text-avenir { font-family: 'AvenirNextLTPro-BoldItalic'; }
.text-agency { font-family: 'AgencyFB-Reg'; }
/* Testing a real life example with absolute CSS done to make a F-ed up font
like UTC lander look good, which we'll then need to modify positioning and
sizing for in order for it to look good with normal fonts */
.flex-row {
display: flex;
justify-content: space-between;
}
#image-box {
.reference-box {
background: url('https://mason-app-staging.herokuapp.com/images/sports_stadium_generic.jpg');
background-size: cover;
position: relative;
width: $test-font-size * 2;
height: $test-font-size * 1.2;
&:before, &:after {
content: '';
left: $test-font-size * .1;
right: $test-font-size * .1;
position: absolute;
height: 1px;
background: rgba(0,0,0,0.1);
z-index: 5;
}
&:before {
top: $test-font-size * 0.245;
}
&:after {
bottom: $test-font-size * 0.245;
}
.text-background {
position: absolute;
left: ($test-font-size * 0.1);
top: ($test-font-size * 0.1);
width: ($test-font-size * 1.8);
height: ($test-font-size * 1);
background:#39b510;
color: #fff;
text-transform: uppercase;
display: flex;
align-items: center;
justify-content: center;
}
.text-container {
margin-top: -10px; // Will be overwritten anyway
text-align: center;
font-size: $test-font-size;
line-height: 1;
}
}
}
#comparison-output {
background: #fff;
padding: 20px;
margin-top: 40px;
flex: 1;
}
//* Debug output from the first example
#sample-output-container {
// * {
// line-height: 1;
// }
> div {
width: 700px;
background: #CCC;
margin-bottom: 20px;
position: relative;
height: 200px;
> .text-container {
background: #fff;
position: absolute;
display: flex;
height: 150px;
left: 25px;
width: 300px;
top: 25px;
align-items: center;
justify-content: center;
> span {
background: #edc79e;
}
}
> .info-box {
font-size: 12px;
font-family: Arial;
background: #fff;
position: absolute;
width: 300px;
top: 25px;
right: 25px;
padding: 10px;
}
}
}
/*
Webfonts
- Code from here down is just base 64'd webfonts.
- All are in "normal" font weight
- Families available:
- 'ARBONNIE';
- 'ADAMCGPRO';
- 'Abolition-Regular';
- 'AgencyFB-Reg';
- 'AvenirNextLTPro-BoldItalic';
- 'UTCLander-Regular';
*/
/* ***** SKIPPING BASE-64'D FONTS FOR STACKOVERFLOW */
JS:
import FontFaceObserver from "https://cdn.skypack.dev/fontfaceobserver#2.1.0";
// var FontFaceObserver = require('fontfaceobserver');
const TYPE_DEFAULT_FONT = 'defaultFont';
const TYPE_CURRENT_FONT = 'currentFont';
// debug output canvases
const removeCalculationCanvases = false;
const allAvailableFonts = [
{ label: 'AR Bonnie', value: 'ARBONNIE' },
{ label: 'Adam CG Pro', value: 'ADAMCGPRO' },
{ label: 'Abolition Regular', value: 'Abolition-Regular' },
{ label: 'Avenir Next LT Pro Bold Italic', value: 'AvenirNextLTPro-BoldItalic' },
{ label: 'Agency FB', value: 'AgencyFB-Reg' },
{ label: 'UTC Lander', value: 'UTCLander-Regular' },
]
const INITIAL_STATE = {
[TYPE_DEFAULT_FONT]: {
label: null,
fontFamily: null,
fontSize: null,
metrics: {},
},
[TYPE_CURRENT_FONT]: {
label: null,
fontFamily: null,
fontSize: null,
metrics: {},
postAdjustmentMetrics: {}
}
}
let state = {
...INITIAL_STATE
}
const _roundToTwo = num => {
return +(Math.round(Number(num) + "e+2") + "e-2");
}
const _roundToFive = num => {
return +(Math.round(Number(num) + "e+5") + "e-5");
}
function timeout(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const getTextMetrics = async(fontFamily, fontSize, testtext = 'Sixty Handgloves ABC') => {
//* For now we'll just keep the test text hard-coded but maybe we'll pass in the element value at some point. (However, being that the text will be editable I don't think that's wise)
testtext = 'Hxy';
const fontSizePx = fontSize.split('px')[0];
//* Generate a hash from the font name for the canvas ID
const canvasId = Math.abs(fontFamily.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0));
console.log('waiting for font to load')
var font = new FontFaceObserver(fontFamily);
await font.load();
console.log('font loaded');
//* Initialize the test canvas so that we can measure stuff
const testCanvasWidth = 400;
const testCanvasHeight = 200;
const testCanvasPadding = 10;
// const canvasDrawingTextFontSize = 1000;
const canvasDrawingTextFontSize = fontSizePx;
const testCanvas = document.createElement('canvas');
testCanvas.id = (`cvs-${canvasId}-${Math.random().toString(36).substring(7)}`);
testCanvas.className = `test-canvas ${canvasId}`;
testCanvas.width = testCanvasWidth;
testCanvas.height = testCanvasHeight;
// document.body.appendChild(testCanvas);
var testCanvasCtx = testCanvas.getContext("2d");
testCanvas.style.font = `${canvasDrawingTextFontSize}px ${fontFamily}`;
testCanvasCtx.font = [`${canvasDrawingTextFontSize}px`, fontFamily].join(' ');
testCanvasCtx.clearRect(0, 0, testCanvasWidth, testCanvasHeight);
testCanvasCtx.fontFamily = fontFamily;
testCanvasCtx.fillStyle = "#fff";
testCanvasCtx.fillRect(0,0,testCanvas.width, testCanvas.height);
testCanvasCtx.fillStyle = "#333333";
testCanvasCtx.fillText(testtext, testCanvasPadding, testCanvasHeight);
// console.log('before timeout');
// await timeout(3000);
// console.log('timeout done');
document.body.appendChild(testCanvas);
//* Get Core Measurements
var xHeight = testCanvasCtx.measureText("x").height;
var capHeight = testCanvasCtx.measureText("H").height;
// var measuredTextMetrics = testCanvasCtx.measureText("Hxy");
var measuredTextMetrics = testCanvasCtx.measureText(testtext);
//* Make the measurements usable (cast to numbers to allow for nulls)
let metrics = {};
metrics.measured = {
actualBoundingBoxAscent: _roundToFive(measuredTextMetrics.actualBoundingBoxAscent),
actualBoundingBoxDescent: _roundToFive(measuredTextMetrics.actualBoundingBoxDescent),
actualBoundingBoxLeft: _roundToFive(measuredTextMetrics.actualBoundingBoxLeft),
actualBoundingBoxRight: _roundToFive(measuredTextMetrics.actualBoundingBoxRight),
fontBoundingBoxAscent: _roundToFive(measuredTextMetrics.fontBoundingBoxAscent),
fontBoundingBoxDescent: _roundToFive(measuredTextMetrics.fontBoundingBoxDescent),
width: _roundToFive(measuredTextMetrics.width)
};
const fontSizeMultiplicand = fontSizePx / canvasDrawingTextFontSize;
const {
actualBoundingBoxAscent,
// actualBoundingBoxDescent,
// actualBoundingBoxLeft,
// actualBoundingBoxRight,
fontBoundingBoxAscent,
fontBoundingBoxDescent,
} = metrics.measured;
metrics.calculated = {
gapAboveText: _roundToFive((fontBoundingBoxAscent - actualBoundingBoxAscent) * fontSizeMultiplicand),
gapBelowText: _roundToFive(fontBoundingBoxDescent * fontSizeMultiplicand),
textHeight: _roundToFive(actualBoundingBoxAscent * fontSizeMultiplicand),
totalHeight: _roundToFive((fontBoundingBoxAscent + fontBoundingBoxDescent) * fontSizeMultiplicand),
};
const {
gapBelowText, gapAboveText, textHeight, totalHeight
} = metrics.calculated;
metrics.calculated.gapBelowTextPercent = _roundToFive(gapBelowText / totalHeight);
metrics.calculated.gapAboveTextPercent = _roundToFive(gapAboveText / totalHeight);
metrics.calculated.gapTopBottomRatio = _roundToFive(gapAboveText / gapBelowText);
metrics.calculated.textHeightPercent = _roundToFive(textHeight / totalHeight);
metrics.calculated.baselineMarginTop = gapBelowText - gapAboveText;
if(removeCalculationCanvases === true){
testCanvas.remove(); // cleanup
}
return metrics;
};
const setFontState = async(fontFamily, fontSize, fontLabel, type = TYPE_CURRENT_FONT) => {
if(fontFamily){
console.log('about to get text metrics')
const metrics = await getTextMetrics(fontFamily, fontSize);
console.log('metrics received');
state[type] = {
label: fontLabel ? fontLabel : fontFamily,
fontFamily,
fontSize,
metrics
}
}
else {
state[type] = {
...INITIAL_STATE[type]
}
}
return true;
}
const watchForFontChange = async() => {
document.addEventListener('input', async(event) => {
if (event.target.id !== 'font-picker') return; // Only run on the font change menu
let label = null;
if(
event.target.options.length &&
typeof event.target.options[event.target.selectedIndex] !== 'undefined' &&
event.target.options[event.target.selectedIndex].text
) {
label = event.target.options[event.target.selectedIndex].text;
}
// For now just grab font size from the default font state, but probably will change later
const fontFamily = event.target.value;
const fontSize = state[TYPE_DEFAULT_FONT].fontSize;
await setFontState(fontFamily, fontSize, label);
console.log('font changed', state);
//* Set the font families in the display
if(fontFamily){
document.getElementById(`unadjusted-text`).style.fontFamily = fontFamily;
document.getElementById(`adjusted-text`).style.fontFamily = fontFamily;
}
else {
document.getElementById(`unadjusted-text`).style.fontFamily = null;
document.getElementById(`adjusted-text`).style.fontFamily = null;
}
//* Calculate the adjustments for the new font compared to the baseline
// const currentFontSize = parseInt(state.currentFont.fontSize,10);
const defaultFontMetrics = state.defaultFont.metrics;
const currentFontMetrics = state.currentFont.metrics;
// const fontSizeAdjustPx = defaultFontMetrics.calculated.textHeight - currentFontMetrics.calculated.textHeight;
// const fontSizeAdjustPcnt = _roundToFive(fontSizeAdjustPx / currentFontMetrics.calculated.textHeight);
//* Apply the adjustments
// const newFontSize = currentFontSize + (currentFontSize * fontSizeAdjustPcnt);
// console.log('newFontSize', newFontSize);
const textToAdjust = document.getElementById(`adjusted-text`);
// const fontSizeStr = `${newFontSize}px`;
textToAdjust.style.marginTop = `${currentFontMetrics.calculated.baselineMarginTop}px`;
const scaler = document.getElementById('adjusted-text-scaler');
const scale = _roundToTwo(defaultFontMetrics.calculated.textHeight / currentFontMetrics.calculated.textHeight);
scaler.style.transform = `scale(${scale})`;
}, false);
}
const addFontOptionsToDropdown = () => {
const parentSelect = document.getElementById(`font-picker`);
for(let i=0; i < allAvailableFonts.length; i++){
const thisOption = allAvailableFonts[i];
if(thisOption.value){
const label = thisOption.label ? thisOption.label : thisOption.value;
const thisOptionTag = document.createElement("option");
thisOptionTag.value = thisOption.value;
const thisOptionText = document.createTextNode(label);
thisOptionTag.appendChild(thisOptionText);
parentSelect.appendChild(thisOptionTag);
}
}
}
const parseDefaultFont = async() => {
const thisText = document.getElementById(`original-text`);
// We might need to do some special stuff for uppercase vs non-uppercase text
const thisTextStyle = window.getComputedStyle(thisText);
const textTransform = thisTextStyle.getPropertyValue('text-transform');
const marginTop = thisTextStyle.getPropertyValue('margin-top');
console.log('marginTop', marginTop);
const uppercase = textTransform === 'uppercase';
const fontFamily = thisTextStyle.getPropertyValue('font-family');
const fontSize = thisTextStyle.getPropertyValue('font-size');
console.log('fontSize', fontSize);
await setFontState(fontFamily, fontSize, null, TYPE_DEFAULT_FONT);
document.getElementById(`original-text`).style.marginTop = `${state.defaultFont.metrics.calculated.baselineMarginTop}px`;
return !! fontFamily;
}
const init = async() => {
console.log(' ');
console.log(' ');
console.log(' ');
console.log('initialized.');
const defaultFont = await parseDefaultFont();
if(defaultFont){
addFontOptionsToDropdown(); // Parse JSON object into the select html tag
await watchForFontChange();
}
else {
// Handle Error -- for some reason there wasn't a font family for the default text.
}
document.getElementById('font-picker').disabled = false;
console.log('state after init done', state);
}
//* Wait for all the base 64'd fonts to load before we run it
document.addEventListener("DOMContentLoaded", (ready => {
init();
// setTimeout(function(){ init(); }, 1000);
}));
Think of the font-size not as the actual size of the individual characters themselves, but as the size of the blocks that contain each character, just like typeset letters:
The size of the blocks is defined in your CSS (using px, pts, ems, etc) but the actual size of the characters within those blocks can vary depending on the font used.
The actual, physical height of any given portion of the font depends on the user-defined DPI setting, current element font-size, and the particular font being used.
https://en.wikipedia.org/wiki/Em_(typography)#CSS
You can use the font-size-adjust property to help alter one of those fonts to scale it closer to the other: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust although its support is currently limited to Firefox: http://caniuse.com/#feat=font-size-adjust
Font size is the size of the glyph from the ascender, such as the top of the letter 'h', to the descender, such as the bottom of the letter 'g'. If you set your font size to 20px, the length from the top of the letter 'h' to the bottom of the letter 'g' will be 20px. Some letters have terminals or spurs, the ends of a letter, may extend a px or two higher on some letters.
In your example, there is a px difference between the two fonts. The Metamorphous font has a mark above some letters that Gentium does not have and that is what accounts for the height difference.
You can read more here.
EDIT: See here with the "caron" above the C compared to the two Gentium letters on the right.
you should rather use something like rem then px :) as rem is a relative measure unit and px is absolute. But fonts always have a different size and imo its not possible what you want to achieve.
Related
I'm using code mirror from ngx-codemirror. I want to split the line when it fits to the width of the parent. I have found some solutions to split the like using,
lineWrapping: true
and in styles
.CodeMirror-wrap pre {
word-break: break-word;
}
Using this I was able to split the line but I need to show the line number too.
The line number is not shown for the line that was just split.
This is the stackblitz link to my issue : code-mirror-line-break-issue
Screenshot :
Please help me with this.
This is not feasible using Code Mirror options, as this is something that is a bit counter intuitive that is rarely (ever?) wanted.
Like I said in my comment, say 2 persons discussing on a phone/web chat about a piece of code/json. They will not see the same thing when one mentions a line number to the other if they have different windows/screen sizes
Solution
As a hack, you can create your own elements representing line numbers and place them over the default line numbers.
Here is the stackblitz demo
Note: This a a very basic example. If you change code mirror settings (font size, gutters,...), you might need to tweak the css or do more calculation based on these settings.
component.html
<div class='codeMirrorContainer'>
<ngx-codemirror
#codeMirror
[options]="codeMirrorOptions"
[(ngModel)]="codeObj"
></ngx-codemirror>
<ul class='lineContainer' [style.top.px]="-topPosition">
<li [style.width.px]='lineWidth' *ngFor="let line of lines">{{line}}</li>
</ul>
</div>
component.css
li
{
height: 19px;
list-style: none;
}
.codeMirrorContainer
{
position:relative;
overflow: hidden;
}
.lineContainer
{
position:absolute;
top:0;
left:0;
margin: 0;
padding: 5px 0 0 0;
text-align: center;
}
::ng-deep .CodeMirror-linenumber
{
visibility: hidden; /* Hides default line numbers */
}
component.ts
export class AppComponent
{
#ViewChild('codeMirror') codeMirrorCmpt: CodemirrorComponent;
private lineHeight: number;
public lineWidth;
public topPosition: number;
public lines = [];
codeMirrorOptions: any = ....;
codeObj :any = ...;
constructor(private cdr: ChangeDetectorRef)
{
}
ngAfterViewInit()
{
this.codeMirrorCmpt.codeMirror.on('refresh', () => this.refreshLines());
this.codeMirrorCmpt.codeMirror.on('scroll', () => this.refreshLines());
setTimeout(() => this.refreshLines(), 500)
}
refreshLines()
{
let editor = this.codeMirrorCmpt.codeMirror;
let height = editor.doc.height;
this.lineHeight = editor.display.cachedTextHeight ? editor.display.cachedTextHeight : this.lineHeight;
if (!this.lineHeight)
{
return;
}
let nbLines = Math.round(height / this.lineHeight);
this.lines = Array(nbLines).fill(0).map((v, idx) => idx + 1);
this.lineWidth = editor.display.lineNumWidth;
this.topPosition = document.querySelector('.CodeMirror-scroll').scrollTop;
this.cdr.detectChanges();
}
}
Background: trying to create an element for easily embedding Font Awesome 5.10.2 Duotone icons into any piece of HTML.
This icon element uses HTML attributes which should map to a specific icon where the mapping is purely controlled by the CSS author.
<x pay></x> <!-- <- icon value for pay should be customizable by CSS author -->
Below is my solution but I wonder ...
Can one reduce
x {
position: relative;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-style: normal;
font-variant: normal;
text-rendering: auto;
white-space: nowrap;
font-family: var(--fa-5-d);
font-weight: var(--fa-d);
background: var(--x-background);
line-height: 1em !important;
}
x::after { position: absolute; left: 0; bottom: 0; }
x::before { color: var(--fa-primary-color, inherit); opacity: 1; opacity: var(--fa-primary-opacity, 1.0); }
x::after { color: var(--fa-secondary-color, inherit); opacity: var(--fa-secondary-opacity, 0.4); }
x:before { --fa-credit-card: "\f09d"; }
x:after { --fa-credit-card: "\10f09d"; }
<x pay></x>
this ↓
x[pay]:before,
x[pay]:after { content: var(--fa-credit-card); }
to this ↓ (avoiding x[pay]:before, x[pay]:after repetition)
x[pay] { --content: var(--fa-credit-card); }
in essence
set a CSS variable once on a parent to a value v
that diverges into different child values v₁ and v₂ related to v?
?
A convenience improvement using web components / custom elements:
<link href='//cdn.blue/{fa-5.10.2}/css/all.css' rel=stylesheet>
<link href='//cdn.blue/{fa+}/var.css' rel=stylesheet>
<link href='//cdn.blue/{fa+}/x-i.css' rel=stylesheet>
<script src='//cdn.blue/<shin>/shin-element.js'></script>
<script>
class XI extends ShinElement {
constructor() {
super(`<style></style>`);
ShinElement.IPA(this, 'jsUpdate', { a: 'js-update', t: ShinElement.Number0 });
}
connectedCallback() { XI.css(this); }
static css(t) {
const c = getComputedStyle(t);
const i = c.getPropertyValue('--i').trim();
const s = t._.QS("style");
s.textContent = `:host:before, :host:after { content: var(${i}); }`;
}
static get observedAttributes() { return [ 'js-update' ]; }
attributeChangedCallback(a, o, n) {
switch (a) {
case "js-update":
const u = this.jsUpdate;
if (u > 0) this._jsu = setInterval(XI.css, u, this);
else { clearInterval(this._jsu); delete this._jsu; }
break;
}
}
}
XI.define();
</script>
<style>x-i[pay] { --i: --fa-user-edit; }</style>
<x-i pay js-update=200 id=x></x-i>
https://cdn.blue/<shin>/docs/ShinElement.html
https://codepen.io/cetinsert/pen/QWLZgwZ?editors=1000
The need for js-update (HTML), jsUpdate (JS) for doing live CSS edits is unfortunate.
x.jsUpdate = 0; // to disable getComputedStyle updates
Thus I still wonder if there is a better solution - be it CSS-only or using web components.
I am attempting to create a map with multiple kml layers that can be toggled with the google places search, but have been unsuccessful. The purpose of this is to have the ability to search an address or intersection and determine the correct response apparatus for fire or medical calls for a dispatch center, with the search being biased toward the correct county. I have the base map with just one layer working here, but I've been unable to correctly integrate the kml layer toggle with the google places search. I had received some good input from geocodezip on using the geocode function here, but I wasn't able to bias it towards the correct location or include the predictive search completion. I also like the way that my original map will zoom and center on the searched address and would like to maintain that functionality. Admittedly, I am very much lacking in ability with this, and everything I have done has been to simply copy and modify working examples that others have posted, but I have not found any working examples that include both the google places search and kml layers with toggle. This is what I have so far. Any help in getting this to work would be very much appreciated.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<style>
html, body {
height: 100%;
margin: 0;
padding: 0;
}
#map {
height: 100%;
}
.controls {
margin-top: 10px;
border: 1px solid transparent;
border-radius: 2px 0 0 2px;
box-sizing: border-box;
-moz-box-sizing: border-box;
height: 32px;
outline: none;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
#pac-input {
background-color: #fff;
font-family: Roboto;
font-size: 15px;
font-weight: 300;
margin-left: 12px;
padding: 0 11px 0 13px;
text-overflow: ellipsis;
width: 300px;
}
#pac-input:focus {
border-color: #4d90fe;
}
.pac-container {
font-family: Roboto;
}
#type-selector {
color: #fff;
background-color: #4d90fe;
padding: 5px 11px 0px 11px;
}
#type-selector label {
font-family: Roboto;
font-size: 13px;
font-weight: 300;
}
</style>
<title>Advanced Backup Map</title>
<style>
#target {
width: 345px;
}
</style>
<script>
var kml = {
a: {
name: "Fire Response Areas",
url: "https://drive.google.com/uc?export=download&id=0B2gbIV1dXlvDVDhVLXc2N1Y5ZEE"
},
b: {
name: "SD Counties",
url: "https://drive.google.com/uc?export=download&id=0B2gbIV1dXlvDRndwdEpKTjRBeTA"
},
c: {
name: "Counties in Surrounding States",
url: "https://drive.google.com/uc?export=download&id=0B2gbIV1dXlvDRlhLTm93S2Y3eDQ"
}
// keep adding more if ye like
};
// This example adds a search box to a map, using the Google Place Autocomplete
// feature. People can enter geographical searches. The search box will return a
// pick list containing a mix of places and predicted search terms.
function initAutocomplete() {
// lets define some vars to make things easier later
var options = {
center: new google.maps.LatLng(43.64837, -96.73737),
zoom: 10,
mapTypeId: google.maps.MapTypeId.ROADMAP
}
var map = new google.maps.Map(document.getElementById('map'), options);
createTogglers();
// Append Class on Select
function highlight(box, listitem) {
var selected = 'selected';
var normal = 'normal';
document.getElementById(listitem).className = (box.checked ? selected : normal);
};
function startup() {
// for example, this toggles kml a on load and updates the menu selector
var checkit = document.getElementById('a');
checkit.checked = true;
toggleKML(checkit, 'a');
highlight(checkit, 'selector1');
};
// Create the search box and link it to the UI element.
var defaultBounds = new google.maps.LatLngBounds(
new google.maps.LatLng(43.38218, -97.29373),
new google.maps.LatLng(43.92451, -96.34532));
var input = document.getElementById('pac-input');
var searchBox = new google.maps.places.SearchBox(input, {
bounds: defaultBounds
});
map.controls[google.maps.ControlPosition.TOP_LEFT].push(input);
// Bias the SearchBox results towards current map's viewport.
map.addListener('bounds_changed', function() {
searchBox.setBounds(map.getBounds());
});
var markers = [];
// [START region_getplaces]
// Listen for the event fired when the user selects a prediction and retrieve
// more details for that place.
searchBox.addListener('places_changed', function() {
var places = searchBox.getPlaces();
if (places.length == 0) {
return;
}
// Clear out the old markers.
markers.forEach(function(marker) {
marker.setMap(null);
});
markers = [];
// For each place, get the icon, name and location.
var bounds = new google.maps.LatLngBounds();
places.forEach(function(place) {
var icon = {
url: place.icon,
size: new google.maps.Size(71, 71),
origin: new google.maps.Point(0, 0),
anchor: new google.maps.Point(17, 34),
scaledSize: new google.maps.Size(25, 25)
};
// Create a marker for each place.
markers.push(new google.maps.Marker({
map: map,
icon: icon,
title: place.name,
position: place.geometry.location
}));
if (place.geometry.viewport) {
// Only geocodes have viewport.
bounds.union(place.geometry.viewport);
} else {
bounds.extend(place.geometry.location);
}
});
map.fitBounds(bounds);
map.setZoom(16);
map.setCenter(center);
});
// [END region_getplaces]
// the important function... kml[id].xxxxx refers back to the top
function toggleKML(checked, id) {
if (checked) {
var layer = new google.maps.KmlLayer(kml[id].url, {
preserveViewport: true,
suppressInfoWindows: false
});
// store kml as obj
kml[id].obj = layer;
kml[id].obj.setMap(map);
}
else {
kml[id].obj.setMap(null);
delete kml[id].obj;
}
};
// create the controls dynamically because it's easier, really
function createTogglers() {
var html = "<form><ul>";
for (var prop in kml) {
html += "<li id=\"selector-" + prop + "\"><input type='checkbox' id='" + prop + "'" +
" onclick='highlight(this,\"selector-" + prop + "\"); toggleKML(this.checked, this.id)' \/>" +
kml[prop].name + "<\/li>";
}
document.getElementById("toggle_box").innerHTML = html;
};
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBhTQXonppCE2eVDQO5AHy11kMn-o27m_U&libraries=places&callback=initAutocomplete"
async defer></script>
</head>
<body>
<input id="pac-input" class="controls" type="text" placeholder="Search Box">
<div id="map"></div>
<div id="toggle_box" style="position: absolute; top: 10px; right: 10px; padding: 5px; background: #fff; z-index: 1; "></div>
</body>
</html>
I got the canvas working, I'm having issues trying to position it.
Specifically I want to implement them to the same effect as:
html {
background: url(back.jpg) no-repeat center center fixed;
background-size: cover;
}
for static images. Basically no interaction with other elements, and positioned as low as possible with regards to the stacking context. Additionally, I'd like to have the canvas background as compartmentalized / as segmented as possible from the rest of the code.
By segmented, I mean something like this:
<body>
<div id="backgroundContainer">
<canvas id="myCanvas"></canvas>
</div>
<div id="everythingElseContainer">
....
</div>
<script src="canvasAnimation.js"></script>
</body>
or this:
<body>
<div id="container">
<canvas id="myCanvas"></canvas>
<div id="everythingElse">
....
</div>
</div>
<script src="canvasAnimation.js"></script>
</body>
to minimize the possibility of css conflicts.
var WIDTH;
var HEIGHT;
var canvas;
var con;
var g;
var pxs = new Array();
var rint = 60;
$(document).ready(function(){
WIDTH = window.innerWidth;
HEIGHT = window.innerHeight;
canvas = document.getElementById('canvas');
$(canvas).attr('width', WIDTH).attr('height',HEIGHT);
con = canvas.getContext('2d');
for(var i = 0; i < 100; i++) {
pxs[i] = new Circle();
pxs[i].reset();
}
setInterval(draw,rint);
});
function draw() {
con.clearRect(0,0,WIDTH,HEIGHT);
for(var i = 0; i < pxs.length; i++) {
pxs[i].fade();
pxs[i].move();
pxs[i].draw();
}
}
function Circle() {
this.s = {ttl:8000, xmax:5, ymax:2, rmax:10, rt:1, xdef:960, ydef:540, xdrift:4, ydrift: 4, random:true, blink:true};
this.reset = function() {
this.x = (this.s.random ? WIDTH*Math.random() : this.s.xdef);
this.y = (this.s.random ? HEIGHT*Math.random() : this.s.ydef);
this.r = ((this.s.rmax-1)*Math.random()) + 1;
this.dx = (Math.random()*this.s.xmax) * (Math.random() < .5 ? -1 : 1);
this.dy = (Math.random()*this.s.ymax) * (Math.random() < .5 ? -1 : 1);
this.hl = (this.s.ttl/rint)*(this.r/this.s.rmax);
this.rt = Math.random()*this.hl;
this.s.rt = Math.random()+1;
this.stop = Math.random()*.2+.4;
this.s.xdrift *= Math.random() * (Math.random() < .5 ? -1 : 1);
this.s.ydrift *= Math.random() * (Math.random() < .5 ? -1 : 1);
}
this.fade = function() {
this.rt += this.s.rt;
}
this.draw = function() {
if(this.s.blink && (this.rt <= 0 || this.rt >= this.hl)) this.s.rt = this.s.rt*-1;
else if(this.rt >= this.hl) this.reset();
var newo = 1-(this.rt/this.hl);
con.beginPath();
con.arc(this.x,this.y,this.r,0,Math.PI*2,true);
con.closePath();
var cr = this.r*newo;
g = con.createRadialGradient(this.x,this.y,0,this.x,this.y,(cr <= 0 ? 1 : cr));
g.addColorStop(0.0, 'rgba(255,255,255,'+newo+')');
g.addColorStop(this.stop, 'rgba(77,101,181,'+(newo*.6)+')');
g.addColorStop(1.0, 'rgba(77,101,181,0)');
con.fillStyle = g;
con.fill();
}
this.move = function() {
this.x += (this.rt/this.hl)*this.dx;
this.y += (this.rt/this.hl)*this.dy;
if(this.x > WIDTH || this.x < 0) this.dx *= -1;
if(this.y > HEIGHT || this.y < 0) this.dy *= -1;
}
this.getX = function() { return this.x; }
this.getY = function() { return this.y; }
}
html, body, div, button, canvas, .containr {
padding: 0;
border: none;
margin: 0;
}
html, body, .containr{
height: 100%;
width: 100%;
background: none;
}
html, body {
font-size: 13px;
text-decoration: none;
font-family: Verdana, Geneva, sans-serif !important;
}
button {
transition: all 0.24s ease;
}
h1 {
font-size: 4rem;
}
button {
font-size: 5.6rem;
}
#pixie {
position:fixed;
z-index: 0;
background: black;
}
.containr>div {
background: blue;
}
.containr {
overflow:hidden;
color: #ffffff;
z-index: 9;
font-size: 256%;
white-space: nowrap;
display: flex;
flex-flow: column nowrap;
justify-content: space-around;
align-items: center;
align-content: center;
}
.btnz {
margin-left: 2.4%;
margin-right: 2.4%;
background: #ffffff;
background: rgba(0, 0, 0, .36);
text-shadow: 1px 1px 3px #000;
padding: 2rem;
}
.btnz:hover {
background: #3cb0fd;
text-shadow: none;
text-decoration: none;
}
/* Outline Out */
.hvr {
display: inline-block;
vertical-align: middle;
-webkit-transform: translateZ(0);
transform: translateZ(0);
box-shadow: 0 0 1px rgba(0, 0, 0, 0);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
-moz-osx-font-smoothing: grayscale;
position: relative;
}
.hvr:before {
content: '';
position: absolute;
border: #e1e1e1 solid 5px;
top: -4px;
right: -4px;
bottom: -4px;
left: -4px;
-webkit-transition-duration: 0.3s;
transition-duration: 0.3s;
-webkit-transition-property: top, right, bottom, left;
transition-property: top, right, bottom, left;
}
.hvr:hover:before, .hvr:focus:before, .hvr:active:before {
top: -18px;
right: -18px;
bottom: -18px;
left: -18px;
border: #ffffff solid 8px;
}
<!doctype html>
<html lang="en">
<head datetime="2015-10-31">
<link rel="stylesheet" href="main.css">
</head>
<body>
<div class="containr">
<canvas id="canvas"></canvas>
<div>
<h1>Main Title</h1>
</div>
<div>
<button class="btnz hvr">
Button Butt
</button>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="js.js"></script>
</body>
</html>
To move objects down in the visual order use the CSS styling z-index smaller numbers move the element down under other elements, higher numbers bring it up.See MDN z-index for more info.
To set the background of an element to a canvas use
element.style.background= "url(" + canvas.toDataURL() + ")";
To isolate of compartmentalize some code the easiest way is to wrap it in a anonymous function and call it. Everything inside it is isolated. Use 'use strict' directive to ensure you do not accidentally create global scoped variables.
A normal anonymous function does nothing and can not be used.
function(){ console.log(42); }; // does nothing
But if you wrap it in () and then add the function call tokens to the end ( ) you can call it like any function.
(function(){ console.log(42); })(); // send the meaning of life,
// the universe, and everything
// to the console.
The function below wraps up a and nothing can get access to a outside the anonymous function.
(function(){
var a = 1;
})();
But you can easily forget to put var in front of a variable making the variable visible to the entire page.
(function(){
var a = 1;
outThere = 2; // Oh no this is has been placed in
// global scope because it is missing
// the var token.
})();
To stop this use the 'use strict' directive.
(function(){
"use strict"; // this must be the very first line of the function
var a = 1;
outThere = 2; // this will cause the javascript to throw a
// ReferenceError: outThere is not defined
})();
It throws an error and stop the function from running but at least you will know that you have a leak.
Everything inside the anonymous function will manage itself. Deleting itself when not needed any more. Or remaining in memory if the Javascript engine holds an internal reference.
The next function starts up and calls its own function doSomething then exits and is deleted completely including the big array.
(function(){
var bigArray = new Array(100000000);
function doSomething(){
console.log("Whats up?");
}
doSomething();
})();
The next one will create a big array and hold that array in memory for 10 seconds (lifeTime). This is because the setTimeout has given the javascript engine an internal reference to doSomething. As long as that reference exists the bigArray will remain (because of closure). After the timeout the reference his no longer need and thus disposed causing all associated referances to go as well and thus disappear. All done via the magic of garbage collection.
Info on Clouser
Info on Garbage collection MDN is out of date but I am sure a quick search on StackOverflow will help.
(function(){
var bigArray = new Array(100000000);
function doSomething(){
console.log("Big Array has had its time.");
}
setTimeout(doSomething,10000);
})();
Attaching an object to items outside the anonymous function scope will expose data in that object to the global scope.
The next function adds a property to a DOM element. This is visible to the global scope and also means that the lifetime of the function will be as long as that element exists.
(function(){
function Info(){
... create info ..
}
var element = document.getElementById("thisOutsideWorld");
var importantPrivateInfo = new Info();
element.keepThis = importantPrivateInfo;
})();
But this does not apply to primitive types as they are copied not referenced. These are Numbers, Strings, Booleans , Undefined, Null...
So to set the background to a canvas via a compartmentalized function see the following function
(function(){
'use strict';
var myCanvas = document.createElement("canvas");
myCanvas .width = 1024;
myCanvas .height =1024;
var ctx = canvas.getContext("2d");
// toDo
// draw the stuff you want.
var el = document.getElementById("myElement");
if(el !== null){
el.style.background = "url("+canvas.toDataURL()+")";
}
// all done
})(); // once run it will delete the canvas and ctx and leave only the copied dataURL
You may think that this exposes the canvas. But it is safe as the canvas is converted to a string and strings are copied not referenced.
If you need to keep the canvas for some period then use a timer to create an internal reference to the anonymous function
The following function will create a canvas and update it every second for 100 seconds. After that it will be deleted and completely gone.
(function(){
'use strict';
var myCanvas = document.createElement("canvas");
myCanvas .width = 1024;
myCanvas .height =1024;
var lifeCounter = 0;
var ctx = canvas.getContext("2d");
// toDo
// draw the stuff you want.
var el = document.getElementById("myElement");
function update(){
// draw stuff on the canvas
if(el !== null){
el.style.background = "url("+canvas.toDataURL()+")";
}
lifeCounter += 1;
if(lifeCounter < 100){
setTimeout(update,1000);
}
}
update(); //start the updates
// all done
})();
Hope this helps.
I have two CSS classes:
.class1 {
height: 100%;
width: 300px;
border: 1px none #B0B0B0;
position: relative;
display: inline;
left: 10px;
}
.class2 {
height: 100%;
width: 200px;
position: relative;
display: inline;
margin-left: 15px;
background-color: #00CCCC;
border-top-width: 1px;
border-right-width: 1px;
border-bottom-width: 1px;
border-left-width: 1px;
border-top-style: solid;
border-right-style: solid;
border-bottom-style: solid;
border-left-style: solid;
}
Now, as you can see, they're both set to display in a line (no line breaks in between elements). Which works correctly. But for some reason, ever since I set the display to inline, the Padding, the Positioning and the Margin CSS have all just stopped working. I can add a margin-left 10inches and nothing will happen. Same with padding and positioning.
Can anyone explain how to fix this?
Also, I have the relative position set on both classes, yet when viewing the page in a browser, .class2 over laps .class1 when its supposed to be just after .class1.
Any ideas?
EDIT:
Okay, so I've done a JSFiddle, but it seems to be playing up even more there....
Looks like the Width is not working....
here it is:
http://jsfiddle.net/zYbwh/1/
You need to use
display: inline-block;
instead. margin doesn't work with display: inline elements, however with inline-block it does. You can then have an inline element with margins and explicit widths/heights.
To make this work in IE7, add these two lines:
*display: inline;
zoom: 1;
It's horrible, but it works.
I know this is quite a late answer but I wrote a jQuery plugin which support padding on inline elements (with word breaking) see this JSfiddle:
http://jsfiddle.net/RxKek/
Plugin Code:
$.fn.outerHTML = function () {
// IE, Chrome & Safari will comply with the non-standard outerHTML, all others (FF) will have a fall-back for cloning
return (!this.length) ? this : (this[0].outerHTML || (
function (el) {
var div = document.createElement('div');
div.appendChild(el.cloneNode(true));
var contents = div.innerHTML;
div = null;
return contents;
})(this[0]));
};
/*
Requirements:
1. The container must NOT have a width!
2. The element needs to be formatted like this:
<div>text</div>
in stead of this:
<div>
text
</div>
*/
$.fn.fixInlineText = function (opt) {
return this.each(function () {
//First get the container width
var maxWidth = opt.width;
//Then get the width of the inline element
//To calculate the correct width the element needs to
//be 100% visible that's why we make it absolute first.
//We also do this to the container.
$(this).css("position", "absolute");
$(this).parent().css("position", "absolute").css("width", "200%");
var width = $(this).width();
$(this).css("position", "");
$(this).parent().css("position", "").css("width", "");
//Don't do anything if it fits
if (width < maxWidth) {
return;
}
//Check how many times the container fits within the box
var times = Math.ceil(width / maxWidth);
//Function for cleaning chunks
var cleanChunk = function (chunk) {
var thisChunkLength = chunk.length - 1;
if (chunk[0] == " ") chunk = chunk.substring(1);
if (chunk[thisChunkLength] == " ") chunk = chunk.substring(0, thisChunkLength);
return chunk;
};
//Divide the text into chunks
var text = $(this).html();
var textArr = text.split(" ");
var chunkLength = Math.ceil((textArr.length - 1) / times);
var chunks = [];
var curChunk = "";
var curChunkCount = 0;
var isParsingHtml = false;
//Loop through the text array and split it into chunks
for (var i in textArr) {
//When we are parsing HTML we don't want to count the
//spaces since the user doesn't see it.
if (isParsingHtml) {
//Check for a HTML end tag
if (/<\/[a-zA-Z]*>/.test(textArr[i]) || /[a-zA-Z]*>/.test(textArr[i])) {
isParsingHtml = false;
}
} else {
//Check for a HTML begin tag
if (/<[a-zA-Z]*/.test(textArr[i])) {
isParsingHtml = true;
}
}
//Calculate chunks
if (curChunkCount == (chunkLength - 1) && !isParsingHtml) {
curChunk += textArr[i] + " ";
chunks.push(cleanChunk(curChunk));
curChunk = "";
curChunkCount = -1;
} else if ((i == (textArr.length - 1))) {
curChunk += textArr[i];
chunks.push(cleanChunk(curChunk));
break;
} else {
curChunk += textArr[i] + " ";
}
if (!isParsingHtml) {
curChunkCount++;
}
}
//Convert chunks to new elements
var el = $($(this).html("").outerHTML());
for (var x in chunks) {
var new_el = el.clone().html(chunks[x]).addClass("text-render-el");
var new_el_container = $("<div/>").addClass("text-render-container");
new_el_container.append(new_el);
$(this).before(new_el_container);
}
//Finally remove the current element
$(this).remove();
});
};
Thats the problem you get when using templates, ive programmed a site in php, but the design is killing me.
So i try'd some rocket fuel for webdesigners.
And this is the problems i keep getting every step of the way...
Inline-block does not work for me, nothing works, becouse it is not my design and i dont know the setup.
Ive tryd doing the design myself, but i am out of time, i need a design yesterday.
I suggest you take what u need from the templates and delete everything else, that will schrink your problem, and save you time.