Created
September 15, 2023 11:02
-
-
Save fabiospampinato/07fa26e020cd3ed96b454ab41e27b933 to your computer and use it in GitHub Desktop.
A little Voby hook for rendering rulers around an element on the page
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* IMPORT */ | |
import {$} from 'voby'; | |
import {useCanvasOverlay, useEffect, useEventListener, useRect, useResolved} from '~/hooks'; | |
/* TYPES */ | |
type Line = { | |
offset: number | |
horizontal: boolean | |
}; | |
type Pill = { | |
line?: Line, | |
x: number, | |
y: number | |
}; | |
/* HELPERS */ | |
const BACKGROUND_COLOR = 'transparent'; | |
const COLOR_RULER = '#606060BB'; | |
const COLOR_MARK_SMALL = '#FFFFFF'; | |
const COLOR_MARK_MEDIUM = '#FFFFFF'; | |
const COLOR_MARK_BIG = '#FFFFFF'; | |
const COLOR_LABEL = '#FFFFFF'; | |
const COLOR_LINE = '#2D72D2'; | |
const COLOR_GRID_SMALL = 'transparent'; | |
const COLOR_GRID_MEDIUM = '#80808022'; | |
const COLOR_GRID_BIG = '#80808033'; | |
const COLOR_PILL = '#101010BB'; | |
const COLOR_PILL_LABEL = '#FFFFFF'; | |
const GRID_TICKNESS = 1; | |
const LINE_TICKNESS = 1; | |
const RULER_TICKNESS = 22; | |
const MARK_TICKNESS = 1; | |
const MARK_SIZE_SMALL = 4; | |
const MARK_SIZE_MEDIUM = 8; | |
const MARK_SIZE_BIG = 12; | |
const MARK_GAP_SMALL = 5; | |
const MARK_GAP_MEDIUM = 20; | |
const MARK_GAP_BIG = 100; | |
const LABEL_SIZE = 10; | |
const LABEL_OFFSET = 19; | |
const PILL_LABEL_SIZE = 12; | |
const PILL_OFFSET = 6; | |
const PILL_PADDING_X = 12; | |
const PILL_PADDING_Y = 8; | |
const PILL_RADIUS = 12; | |
const FONT_FAMILY = 'sans-serif'; | |
/* MAIN */ | |
//TODO: Support moving an existing line | |
//TODO: Support deleting an existing line | |
const useRulers = ( ref: $<HTMLElement | undefined> = document.documentElement ): void => { | |
const canvas = useCanvasOverlay (); | |
const ctx = canvas.getContext ( '2d' ); | |
const grid = $(false); | |
const lines = $<Line[]>( [], { equals: false } ); | |
const pill = $<Pill>(); | |
const rect = useRect ( ref ); | |
if ( !ctx ) return; | |
const clear = (): void => { | |
ctx.clearRect ( 0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER ); | |
}; | |
const paintBackground = ( x: number, y: number, width: number, height: number ): void => { | |
ctx.fillStyle = BACKGROUND_COLOR; | |
ctx.fillRect ( RULER_TICKNESS, RULER_TICKNESS, width, height ); | |
}; | |
const paintLines = ( x: number, y: number, width: number, height: number ): void => { | |
for ( const line of lines () ) { | |
if ( line.offset <= RULER_TICKNESS ) continue; | |
ctx.fillStyle = COLOR_LINE; | |
if ( line.horizontal ) { | |
ctx.fillRect ( x + RULER_TICKNESS, y + line.offset - LINE_TICKNESS, width - RULER_TICKNESS, LINE_TICKNESS ); | |
} else { | |
ctx.fillRect ( x + line.offset - LINE_TICKNESS, y + RULER_TICKNESS, LINE_TICKNESS, height - RULER_TICKNESS ); | |
} | |
} | |
}; | |
const paintPill = ( x: number, y: number, width: number, height: number ): void => { | |
useResolved ( pill, pill => { | |
if ( !pill?.line ) return; | |
ctx.font = `${PILL_LABEL_SIZE}px ${FONT_FAMILY}`; | |
const label = `${pill.line.offset}px`; | |
const measure = ctx.measureText ( label ); | |
/* BACKGROUND */ | |
const height = PILL_PADDING_Y + LABEL_SIZE + PILL_PADDING_Y; | |
const width = PILL_PADDING_X + measure.width + PILL_PADDING_X; | |
const x = pill.x + PILL_OFFSET; | |
const y = pill.y - height - PILL_OFFSET; | |
ctx.fillStyle = COLOR_PILL; | |
ctx.roundRect ( x, y, width, height, PILL_RADIUS ); | |
ctx.fill (); | |
/* LABEL */ | |
const tx = x + PILL_PADDING_X; | |
const ty = y + PILL_PADDING_Y + PILL_LABEL_SIZE -3; //UGLY | |
ctx.fillStyle = COLOR_PILL_LABEL; | |
ctx.fillText ( label, tx, ty ); | |
}); | |
}; | |
const paintCornerRuler = ( x: number, y: number, width: number, height: number ): void => { | |
ctx.fillStyle = COLOR_RULER; | |
ctx.translate ( RULER_TICKNESS, RULER_TICKNESS ); | |
ctx.moveTo ( 0, 0 ); | |
ctx.lineTo ( 5, 0 ); | |
ctx.lineTo ( 5, 0 ); | |
ctx.bezierCurveTo ( 2.75, 0, 0, 2.75, 0, 5 ); | |
ctx.lineTo ( 0, 5 ); | |
ctx.lineTo ( 0, 0 ); | |
ctx.translate ( -RULER_TICKNESS, -RULER_TICKNESS ); | |
ctx.fill (); | |
}; | |
const paintHorizontalRuler = ( x: number, y: number, width: number, height: number ): void => { | |
ctx.fillStyle = COLOR_RULER; | |
ctx.fillRect ( x + RULER_TICKNESS, y, width - RULER_TICKNESS, RULER_TICKNESS ); | |
}; | |
const paintHorizontalMarks = ( x: number, y: number, width: number, height: number ): void => { | |
for ( let i = MARK_GAP_SMALL; i <= width; i += MARK_GAP_SMALL ) { | |
if ( i % MARK_GAP_BIG === 0 ) { | |
ctx.fillStyle = COLOR_MARK_BIG; | |
ctx.fillRect ( x + i - MARK_TICKNESS, y, MARK_TICKNESS, MARK_SIZE_BIG ); | |
} else if ( i % MARK_GAP_MEDIUM === 0 ) { | |
ctx.fillStyle = COLOR_MARK_MEDIUM; | |
ctx.fillRect ( x + i - MARK_TICKNESS, y, MARK_TICKNESS, MARK_SIZE_MEDIUM ); | |
} else { | |
ctx.fillStyle = COLOR_MARK_SMALL; | |
ctx.fillRect ( x + i - MARK_TICKNESS, y, MARK_TICKNESS, MARK_SIZE_SMALL ); | |
} | |
} | |
}; | |
const paintHorizontalLabels = ( x: number, y: number, width: number, height: number ): void => { | |
ctx.fillStyle = COLOR_LABEL; | |
ctx.font = `${LABEL_SIZE}px ${FONT_FAMILY}`; | |
for ( let i = MARK_GAP_BIG; i < width + MARK_GAP_BIG; i += MARK_GAP_BIG ) { | |
const label = `${i}`; | |
const measure = ctx.measureText ( label ); | |
ctx.fillText ( label, x + i - measure.width / 2, y + LABEL_OFFSET ); | |
} | |
}; | |
const paintHorizontalGridSmall = ( x: number, y: number, width: number, height: number ): void => { | |
for ( let i = MARK_GAP_SMALL; i <= width; i += MARK_GAP_SMALL ) { | |
if ( i <= RULER_TICKNESS ) continue; | |
ctx.fillStyle = COLOR_GRID_SMALL; | |
ctx.fillRect ( x + i - GRID_TICKNESS, y + RULER_TICKNESS, GRID_TICKNESS, height - RULER_TICKNESS ); | |
} | |
}; | |
const paintHorizontalGridMedium = ( x: number, y: number, width: number, height: number ): void => { | |
for ( let i = MARK_GAP_MEDIUM; i <= width; i += MARK_GAP_MEDIUM ) { | |
if ( i <= RULER_TICKNESS ) continue; | |
ctx.fillStyle = COLOR_GRID_MEDIUM; | |
ctx.fillRect ( x + i - GRID_TICKNESS, y + RULER_TICKNESS, GRID_TICKNESS, height - RULER_TICKNESS ); | |
} | |
}; | |
const paintHorizontalGridBig = ( x: number, y: number, width: number, height: number ): void => { | |
for ( let i = MARK_GAP_BIG; i <= width; i += MARK_GAP_BIG ) { | |
if ( i <= RULER_TICKNESS ) continue; | |
ctx.fillStyle = COLOR_GRID_BIG; | |
ctx.fillRect ( x + i - GRID_TICKNESS, y + RULER_TICKNESS, GRID_TICKNESS, height - RULER_TICKNESS ); | |
} | |
}; | |
const paintVerticalRuler = ( x: number, y: number, width: number, height: number ): void => { | |
ctx.fillStyle = COLOR_RULER; | |
ctx.fillRect ( x, y, RULER_TICKNESS, height ); | |
}; | |
const paintVerticalMarks = ( x: number, y: number, width: number, height: number ): void => { | |
for ( let i = MARK_GAP_SMALL; i < height; i += MARK_GAP_SMALL ) { | |
if ( i % MARK_GAP_BIG === 0 ) { | |
ctx.fillStyle = COLOR_MARK_BIG; | |
ctx.fillRect ( x, y + i - MARK_TICKNESS, MARK_SIZE_BIG, MARK_TICKNESS ); | |
} else if ( i % MARK_GAP_MEDIUM === 0 ) { | |
ctx.fillStyle = COLOR_MARK_MEDIUM; | |
ctx.fillRect ( x, y + i - MARK_TICKNESS, MARK_SIZE_MEDIUM, MARK_TICKNESS ); | |
} else { | |
ctx.fillStyle = COLOR_MARK_SMALL; | |
ctx.fillRect ( x, y + i - MARK_TICKNESS, MARK_SIZE_SMALL, MARK_TICKNESS ); | |
} | |
} | |
}; | |
const paintVerticalLabels = ( x: number, y: number, width: number, height: number ): void => { | |
ctx.rotate ( ( Math.PI / 180 ) * -90 ); | |
ctx.fillStyle = COLOR_LABEL; | |
ctx.font = `${LABEL_SIZE}px ${FONT_FAMILY}`; | |
for ( let i = MARK_GAP_BIG; i < height + MARK_GAP_BIG; i += MARK_GAP_BIG ) { | |
const label = `${i}`; | |
const measure = ctx.measureText ( label ); | |
ctx.fillText ( label, - y - i - measure.width / 2, x + LABEL_OFFSET ); | |
} | |
ctx.rotate ( ( Math.PI / 180 ) * 90 ); | |
}; | |
const paintVerticalGridSmall = ( x: number, y: number, width: number, height: number ): void => { | |
for ( let i = MARK_GAP_SMALL; i < height; i += MARK_GAP_SMALL ) { | |
if ( i <= RULER_TICKNESS ) continue; | |
ctx.fillStyle = COLOR_GRID_SMALL; | |
ctx.fillRect ( x + RULER_TICKNESS, y + i - GRID_TICKNESS, width - RULER_TICKNESS, GRID_TICKNESS ); | |
} | |
}; | |
const paintVerticalGridMedium = ( x: number, y: number, width: number, height: number ): void => { | |
for ( let i = MARK_GAP_MEDIUM; i < height; i += MARK_GAP_MEDIUM ) { | |
if ( i <= RULER_TICKNESS ) continue; | |
ctx.fillStyle = COLOR_GRID_MEDIUM; | |
ctx.fillRect ( x + RULER_TICKNESS, y + i - GRID_TICKNESS, width - RULER_TICKNESS, GRID_TICKNESS ); | |
} | |
}; | |
const paintVerticalGridBig = ( x: number, y: number, width: number, height: number ): void => { | |
for ( let i = MARK_GAP_BIG; i < height; i += MARK_GAP_BIG ) { | |
if ( i <= RULER_TICKNESS ) continue; | |
ctx.fillStyle = COLOR_GRID_BIG; | |
ctx.fillRect ( x + RULER_TICKNESS, y + i - GRID_TICKNESS, width - RULER_TICKNESS, GRID_TICKNESS ); | |
} | |
}; | |
const paint = ( x: number, y: number, width: number, height: number ): void => { | |
ctx.beginPath (); | |
paintBackground ( x, y, width, height ); | |
paintHorizontalRuler ( x, y, width, height ); | |
paintVerticalRuler ( x, y, width, height ); | |
paintCornerRuler ( x, y, width, height ); | |
paintHorizontalMarks ( x, y, width, height ); | |
paintVerticalMarks ( x, y, width, height ); | |
paintHorizontalLabels ( x, y, width, height ); | |
paintVerticalLabels ( x, y, width, height ); | |
if ( grid () ) { | |
paintHorizontalGridSmall ( x, y, width, height ); | |
paintVerticalGridSmall ( x, y, width, height ); | |
paintHorizontalGridMedium ( x, y, width, height ); | |
paintVerticalGridMedium ( x, y, width, height ); | |
paintHorizontalGridBig ( x, y, width, height ); | |
paintVerticalGridBig ( x, y, width, height ); | |
} | |
paintLines ( x, y, width, height ); | |
paintPill ( x, y, width, height ); | |
ctx.closePath (); | |
}; | |
useEffect ( () => { | |
useResolved ( rect, rect => { | |
clear (); | |
paint ( rect.x, rect.y, rect.width, rect.height ); | |
}); | |
}); | |
useEventListener ( ref, 'click', event => { // Toggling the grid on corner click | |
const {clientX, clientY} = event; | |
useResolved ( rect, rect => { | |
if ( clientX < rect.x || clientX > rect.x + RULER_TICKNESS ) return; | |
if ( clientY < rect.y || clientY > rect.y + RULER_TICKNESS ) return; | |
grid ( prev => !prev ); | |
}); | |
}); | |
useEventListener ( ref, 'mousedown', ({ clientX, clientY }) => { // Creating a new line | |
useResolved ( rect, rect => { | |
const isHorizontalX = ( clientX >= rect.x + RULER_TICKNESS ) && ( clientX <= rect.x + RULER_TICKNESS + rect.width ); | |
const isHorizontalY = ( clientY >= rect.y ) && ( clientY <= rect.y + RULER_TICKNESS ); | |
const isHorizontal = isHorizontalX && isHorizontalY; | |
const isVerticalX = ( clientX >= rect.x ) && ( clientX <= rect.x + RULER_TICKNESS ); | |
const isVerticalY = ( clientY >= rect.y + RULER_TICKNESS ) && ( clientY <= rect.y + RULER_TICKNESS + rect.height ); | |
const isVertical = isVerticalX && isVerticalY; | |
if ( !isHorizontal && !isVertical ) return; | |
const offset = isHorizontal ? clientY - rect.y : clientX - rect.x; | |
const horizontal = isHorizontal; | |
const line: Line = { offset, horizontal }; | |
lines ( prev => [...prev, line] ); | |
pill ({ line, x: clientX, y: clientY }); | |
const disposeMouseMove = useEventListener ( ref, 'mousemove', ({ clientX, clientY }) => { // Moving the line | |
line.offset = isHorizontal ? clientY - rect.y : clientX - rect.x; | |
lines ( lines () ); | |
pill ({ line, x: clientX, y: clientY }); | |
}); | |
const disposeMouseUp = useEventListener ( ref, 'mouseup', () => { // Stopping listening | |
pill ( undefined ); | |
disposeMouseMove (); | |
disposeMouseUp (); | |
}); | |
}); | |
}); | |
}; | |
/* EXPORT */ | |
export default useRulers; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment