|
<script lang="ts" context="module"> |
|
export type RulerDirection = "Horizontal" | "Vertical"; |
|
</script> |
|
|
|
<script lang="ts"> |
|
import { onMount } from "svelte"; |
|
|
|
const RULER_THICKNESS = 16; |
|
const MAJOR_MARK_THICKNESS = 16; |
|
const MINOR_MARK_THICKNESS = 6; |
|
const MICRO_MARK_THICKNESS = 3; |
|
|
|
export let direction: RulerDirection = "Vertical"; |
|
export let origin: number; |
|
export let numberInterval: number; |
|
export let majorMarkSpacing: number; |
|
export let minorDivisions = 5; |
|
export let microDivisions = 2; |
|
|
|
let rulerInput: HTMLDivElement | undefined; |
|
let rulerLength = 0; |
|
let svgBounds = { width: "0px", height: "0px" }; |
|
|
|
$: svgPath = computeSvgPath(direction, origin, majorMarkSpacing, minorDivisions, microDivisions, rulerLength); |
|
$: svgTexts = computeSvgTexts(direction, origin, majorMarkSpacing, numberInterval, rulerLength); |
|
|
|
function computeSvgPath(direction: RulerDirection, origin: number, majorMarkSpacing: number, minorDivisions: number, microDivisions: number, rulerLength: number): string { |
|
const isVertical = direction === "Vertical"; |
|
const lineDirection = isVertical ? "H" : "V"; |
|
|
|
const offsetStart = mod(origin, majorMarkSpacing); |
|
const shiftedOffsetStart = offsetStart - majorMarkSpacing; |
|
|
|
const divisions = majorMarkSpacing / minorDivisions / microDivisions; |
|
const majorMarksFrequency = minorDivisions * microDivisions; |
|
|
|
let dPathAttribute = ""; |
|
let i = 0; |
|
for (let location = shiftedOffsetStart; location < rulerLength; location += divisions) { |
|
let length; |
|
if (i % majorMarksFrequency === 0) length = MAJOR_MARK_THICKNESS; |
|
else if (i % microDivisions === 0) length = MINOR_MARK_THICKNESS; |
|
else length = MICRO_MARK_THICKNESS; |
|
i += 1; |
|
|
|
const destination = Math.round(location) + 0.5; |
|
const startPoint = isVertical ? `${RULER_THICKNESS - length},${destination}` : `${destination},${RULER_THICKNESS - length}`; |
|
dPathAttribute += `M${startPoint}${lineDirection}${RULER_THICKNESS} `; |
|
} |
|
|
|
return dPathAttribute; |
|
} |
|
|
|
function computeSvgTexts(direction: RulerDirection, origin: number, majorMarkSpacing: number, numberInterval: number, rulerLength: number): { transform: string; text: string }[] { |
|
const isVertical = direction === "Vertical"; |
|
|
|
const offsetStart = mod(origin, majorMarkSpacing); |
|
const shiftedOffsetStart = offsetStart - majorMarkSpacing; |
|
|
|
const svgTextCoordinates = []; |
|
|
|
let labelNumber = (Math.ceil(-origin / majorMarkSpacing) - 1) * numberInterval; |
|
|
|
for (let location = shiftedOffsetStart; location < rulerLength; location += majorMarkSpacing) { |
|
const destination = Math.round(location); |
|
const x = isVertical ? 9 : destination + 2; |
|
const y = isVertical ? destination + 1 : 9; |
|
|
|
let transform = `translate(${x} ${y})`; |
|
if (isVertical) transform += " rotate(270)"; |
|
|
|
const text = numberInterval >= 1 ? `${labelNumber}` : labelNumber.toFixed(Math.abs(Math.log10(numberInterval))).replace(/\.0+$/, ""); |
|
|
|
svgTextCoordinates.push({ transform, text }); |
|
|
|
labelNumber += numberInterval; |
|
} |
|
|
|
return svgTextCoordinates; |
|
} |
|
|
|
export function resize() { |
|
if (!rulerInput) return; |
|
|
|
const isVertical = direction === "Vertical"; |
|
|
|
const newLength = isVertical ? rulerInput.clientHeight : rulerInput.clientWidth; |
|
const roundedUp = (Math.floor(newLength / majorMarkSpacing) + 1) * majorMarkSpacing; |
|
|
|
if (roundedUp !== rulerLength) { |
|
rulerLength = roundedUp; |
|
const thickness = `${RULER_THICKNESS}px`; |
|
const length = `${roundedUp}px`; |
|
svgBounds = isVertical ? { width: thickness, height: length } : { width: length, height: thickness }; |
|
} |
|
} |
|
|
|
|
|
function mod(n: number, m: number): number { |
|
const remainder = n % m; |
|
return Math.floor(remainder >= 0 ? remainder : remainder + m); |
|
} |
|
|
|
onMount(resize); |
|
</script> |
|
|
|
<div class={`ruler-input ${direction.toLowerCase()}`} bind:this={rulerInput}> |
|
<svg style:width={svgBounds.width} style:height={svgBounds.height}> |
|
<path d={svgPath} /> |
|
{#each svgTexts as svgText} |
|
<text transform={svgText.transform}>{svgText.text}</text> |
|
{/each} |
|
</svg> |
|
</div> |
|
|
|
<style lang="scss" global> |
|
.ruler-input { |
|
flex: 1 1 100%; |
|
background: var(--color-2-mildblack); |
|
overflow: hidden; |
|
position: relative; |
|
box-sizing: border-box; |
|
|
|
&.horizontal { |
|
height: 16px; |
|
border-bottom: 1px solid var(--color-5-dullgray); |
|
} |
|
|
|
&.vertical { |
|
width: 16px; |
|
border-right: 1px solid var(--color-5-dullgray); |
|
|
|
svg text { |
|
text-anchor: end; |
|
} |
|
} |
|
|
|
svg { |
|
position: absolute; |
|
|
|
path { |
|
stroke-width: 1px; |
|
stroke: var(--color-5-dullgray); |
|
} |
|
|
|
text { |
|
font-size: 12px; |
|
fill: var(--color-8-uppergray); |
|
} |
|
} |
|
} |
|
</style> |
|
|