@inkra/react 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +213 -2
  3. package/THIRD_PARTY_NOTICES.md +39 -0
  4. package/Yozai-OFL.txt +94 -0
  5. package/dist/Button.d.ts +36 -0
  6. package/dist/Button.js +89 -0
  7. package/dist/PencilIcon.d.ts +22 -0
  8. package/dist/PencilIcon.js +212 -0
  9. package/dist/PencilLine.d.ts +10 -0
  10. package/dist/PencilLine.js +85 -0
  11. package/dist/PencilSurface.d.ts +19 -0
  12. package/dist/PencilSurface.js +19 -0
  13. package/dist/PencilText.d.ts +16 -0
  14. package/dist/PencilText.js +205 -0
  15. package/dist/accordion/Accordion.d.ts +27 -0
  16. package/dist/accordion/Accordion.js +40 -0
  17. package/dist/accordion/index.d.ts +2 -0
  18. package/dist/accordion/index.js +1 -0
  19. package/dist/assets/ui-hand-yozai.woff2 +0 -0
  20. package/dist/breadcrumb/index.d.ts +16 -0
  21. package/dist/breadcrumb/index.js +25 -0
  22. package/dist/calendar/index.d.ts +21 -0
  23. package/dist/calendar/index.js +41 -0
  24. package/dist/content/index.d.ts +106 -0
  25. package/dist/content/index.js +80 -0
  26. package/dist/day-agenda/index.d.ts +43 -0
  27. package/dist/day-agenda/index.js +47 -0
  28. package/dist/day-agenda/layout.d.ts +20 -0
  29. package/dist/day-agenda/layout.js +60 -0
  30. package/dist/display/index.d.ts +32 -0
  31. package/dist/display/index.js +59 -0
  32. package/dist/dropdown-menu/index.d.ts +34 -0
  33. package/dist/dropdown-menu/index.js +40 -0
  34. package/dist/form/EditablePencilText.d.ts +13 -0
  35. package/dist/form/EditablePencilText.js +312 -0
  36. package/dist/form/Field.d.ts +13 -0
  37. package/dist/form/Field.js +26 -0
  38. package/dist/form/Input.d.ts +27 -0
  39. package/dist/form/Input.js +97 -0
  40. package/dist/form/InputAction.d.ts +7 -0
  41. package/dist/form/InputAction.js +24 -0
  42. package/dist/form/Textarea.d.ts +5 -0
  43. package/dist/form/Textarea.js +23 -0
  44. package/dist/form/context.d.ts +12 -0
  45. package/dist/form/context.js +10 -0
  46. package/dist/form/controlProps.d.ts +23 -0
  47. package/dist/form/controlProps.js +20 -0
  48. package/dist/form/index.d.ts +7 -0
  49. package/dist/form/index.js +3 -0
  50. package/dist/form/types.d.ts +6 -0
  51. package/dist/form/types.js +1 -0
  52. package/dist/images/layers-2x.png +0 -0
  53. package/dist/images/layers.png +0 -0
  54. package/dist/images/marker-icon.png +0 -0
  55. package/dist/index.d.ts +28 -0
  56. package/dist/index.js +22 -0
  57. package/dist/input-number/index.d.ts +16 -0
  58. package/dist/input-number/index.js +77 -0
  59. package/dist/internal/InputActionContext.d.ts +1 -0
  60. package/dist/internal/InputActionContext.js +3 -0
  61. package/dist/internal/cache.d.ts +14 -0
  62. package/dist/internal/cache.js +54 -0
  63. package/dist/internal/line.d.ts +4 -0
  64. package/dist/internal/line.js +21 -0
  65. package/dist/internal/paper.d.ts +1 -0
  66. package/dist/internal/paper.js +65 -0
  67. package/dist/internal/random.d.ts +3 -0
  68. package/dist/internal/random.js +30 -0
  69. package/dist/internal/recovery.d.ts +1 -0
  70. package/dist/internal/recovery.js +52 -0
  71. package/dist/internal/stroke.d.ts +24 -0
  72. package/dist/internal/stroke.js +144 -0
  73. package/dist/internal/surfaceRenderer.d.ts +33 -0
  74. package/dist/internal/surfaceRenderer.js +175 -0
  75. package/dist/internal/textQueue.d.ts +1 -0
  76. package/dist/internal/textQueue.js +37 -0
  77. package/dist/internal/textRenderer.d.ts +27 -0
  78. package/dist/internal/textRenderer.js +213 -0
  79. package/dist/internal/travelIcons.d.ts +24 -0
  80. package/dist/internal/travelIcons.js +14 -0
  81. package/dist/internal/useBrowserLayoutEffect.d.ts +2 -0
  82. package/dist/internal/useBrowserLayoutEffect.js +3 -0
  83. package/dist/internal/usePencilSurface.d.ts +3 -0
  84. package/dist/internal/usePencilSurface.js +108 -0
  85. package/dist/internal/useSelectionDrawing.d.ts +2 -0
  86. package/dist/internal/useSelectionDrawing.js +86 -0
  87. package/dist/internal/wash.d.ts +16 -0
  88. package/dist/internal/wash.js +149 -0
  89. package/dist/journey-map/MapScale.d.ts +4 -0
  90. package/dist/journey-map/MapScale.js +52 -0
  91. package/dist/journey-map/index.d.ts +39 -0
  92. package/dist/journey-map/index.js +195 -0
  93. package/dist/journey-map/terrain.d.ts +8 -0
  94. package/dist/journey-map/terrain.js +127 -0
  95. package/dist/overlay/Overlay.d.ts +65 -0
  96. package/dist/overlay/Overlay.js +77 -0
  97. package/dist/overlay/Popover.d.ts +43 -0
  98. package/dist/overlay/Popover.js +45 -0
  99. package/dist/overlay/index.d.ts +4 -0
  100. package/dist/overlay/index.js +2 -0
  101. package/dist/overlay/themeBridge.d.ts +8 -0
  102. package/dist/overlay/themeBridge.js +98 -0
  103. package/dist/pencil.d.ts +10 -0
  104. package/dist/pencil.js +39 -0
  105. package/dist/select/MultipleSelect.d.ts +44 -0
  106. package/dist/select/MultipleSelect.js +124 -0
  107. package/dist/select/SearchableSelect.d.ts +45 -0
  108. package/dist/select/SearchableSelect.js +50 -0
  109. package/dist/select/Select.d.ts +109 -0
  110. package/dist/select/Select.js +205 -0
  111. package/dist/select/index.d.ts +4 -0
  112. package/dist/select/index.js +2 -0
  113. package/dist/selection/Checkbox.d.ts +9 -0
  114. package/dist/selection/Checkbox.js +37 -0
  115. package/dist/selection/RadioGroup.d.ts +23 -0
  116. package/dist/selection/RadioGroup.js +82 -0
  117. package/dist/selection/Switch.d.ts +8 -0
  118. package/dist/selection/Switch.js +36 -0
  119. package/dist/selection/index.d.ts +6 -0
  120. package/dist/selection/index.js +3 -0
  121. package/dist/selection/useNativeChecked.d.ts +13 -0
  122. package/dist/selection/useNativeChecked.js +27 -0
  123. package/dist/slider/index.d.ts +11 -0
  124. package/dist/slider/index.js +45 -0
  125. package/dist/styles.css +3663 -0
  126. package/dist/tabs/index.d.ts +22 -0
  127. package/dist/tabs/index.js +42 -0
  128. package/dist/tokens.css +355 -0
  129. package/dist/tooltip/Tooltip.d.ts +24 -0
  130. package/dist/tooltip/Tooltip.js +32 -0
  131. package/dist/tooltip/index.d.ts +2 -0
  132. package/dist/tooltip/index.js +1 -0
  133. package/package.json +100 -4
@@ -0,0 +1,23 @@
1
+ import type { AriaAttributes } from 'react';
2
+ interface NativeControlState {
3
+ id?: string;
4
+ required?: boolean;
5
+ disabled?: boolean;
6
+ readOnly?: boolean;
7
+ 'aria-describedby'?: string;
8
+ 'aria-errormessage'?: string;
9
+ 'aria-invalid'?: AriaAttributes['aria-invalid'];
10
+ }
11
+ export declare function useResolvedControlProps(props: NativeControlState & {
12
+ invalid?: boolean;
13
+ }): {
14
+ id: string | undefined;
15
+ required: boolean;
16
+ disabled: boolean;
17
+ readOnly: boolean;
18
+ invalid: boolean;
19
+ describedBy: string | undefined;
20
+ errorMessage: string | undefined;
21
+ ariaInvalid: boolean | "true" | "false" | "grammar" | "spelling" | undefined;
22
+ };
23
+ export {};
@@ -0,0 +1,20 @@
1
+ import { mergeAriaIds, useFieldContext } from './context.js';
2
+ export function useResolvedControlProps(props) {
3
+ const field = useFieldContext();
4
+ const resolvedInvalid = Boolean(props.invalid
5
+ || field?.invalid
6
+ || props['aria-invalid'] === true
7
+ || props['aria-invalid'] === 'true');
8
+ const describedBy = mergeAriaIds(props['aria-describedby'], field?.descriptionId, field?.errorId);
9
+ const errorMessage = mergeAriaIds(props['aria-errormessage'], resolvedInvalid ? field?.errorId : undefined);
10
+ return {
11
+ id: field?.controlId ?? props.id,
12
+ required: Boolean(props.required || field?.required),
13
+ disabled: Boolean(props.disabled || field?.disabled),
14
+ readOnly: Boolean(props.readOnly || field?.readOnly),
15
+ invalid: resolvedInvalid,
16
+ describedBy,
17
+ errorMessage,
18
+ ariaInvalid: resolvedInvalid ? true : props['aria-invalid'],
19
+ };
20
+ }
@@ -0,0 +1,7 @@
1
+ export { Field } from './Field.js';
2
+ export type { FieldProps } from './Field.js';
3
+ export { Input } from './Input.js';
4
+ export type { InputProps, InputVariant, InputTextAlign, InputValueChangeDetails, InputClassNames } from './Input.js';
5
+ export { Textarea } from './Textarea.js';
6
+ export type { TextareaProps } from './Textarea.js';
7
+ export type { FieldControlSize, FieldControlStateProps } from './types.js';
@@ -0,0 +1,3 @@
1
+ export { Field } from './Field.js';
2
+ export { Input } from './Input.js';
3
+ export { Textarea } from './Textarea.js';
@@ -0,0 +1,6 @@
1
+ export type FieldControlSize = 'sm' | 'md' | 'lg';
2
+ export interface FieldControlStateProps {
3
+ invalid?: boolean;
4
+ size?: FieldControlSize;
5
+ seed?: string | number;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
Binary file
Binary file
Binary file
@@ -0,0 +1,28 @@
1
+ export { Button, ButtonGroup, ButtonLink } from './Button.js';
2
+ export * from './overlay/index.js';
3
+ export * from './form/index.js';
4
+ export * from './input-number/index.js';
5
+ export * from './slider/index.js';
6
+ export * from './selection/index.js';
7
+ export * from './select/index.js';
8
+ export * from './tabs/index.js';
9
+ export * from './breadcrumb/index.js';
10
+ export * from './tooltip/index.js';
11
+ export * from './accordion/index.js';
12
+ export * from './display/index.js';
13
+ export * from './content/index.js';
14
+ export * from './dropdown-menu/index.js';
15
+ export * from './calendar/index.js';
16
+ export * from './day-agenda/index.js';
17
+ export * from './journey-map/index.js';
18
+ export type { ButtonGroupOrientation, ButtonAlign, ButtonLoadingPosition, ButtonGroupProps, ButtonLinkProps, ButtonProps, ButtonSize, ButtonVariant, } from './Button.js';
19
+ export { PencilLine } from './PencilLine.js';
20
+ export type { PencilLineOrientation, PencilLineProps } from './PencilLine.js';
21
+ export { PencilSurface } from './PencilSurface.js';
22
+ export type { PencilSurfaceProps, PencilSurfaceVariant } from './PencilSurface.js';
23
+ export { PencilText } from './PencilText.js';
24
+ export type { PencilTextProps } from './PencilText.js';
25
+ export { PencilIcon } from './PencilIcon.js';
26
+ export type { PencilIconBadge, PencilIconPath, PencilIconProps } from './PencilIcon.js';
27
+ export { createPencilPath, createPencilPoints } from './pencil.js';
28
+ export type { PencilPathOptions, PencilPoint } from './pencil.js';
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ export { Button, ButtonGroup, ButtonLink } from './Button.js';
2
+ export * from './overlay/index.js';
3
+ export * from './form/index.js';
4
+ export * from './input-number/index.js';
5
+ export * from './slider/index.js';
6
+ export * from './selection/index.js';
7
+ export * from './select/index.js';
8
+ export * from './tabs/index.js';
9
+ export * from './breadcrumb/index.js';
10
+ export * from './tooltip/index.js';
11
+ export * from './accordion/index.js';
12
+ export * from './display/index.js';
13
+ export * from './content/index.js';
14
+ export * from './dropdown-menu/index.js';
15
+ export * from './calendar/index.js';
16
+ export * from './day-agenda/index.js';
17
+ export * from './journey-map/index.js';
18
+ export { PencilLine } from './PencilLine.js';
19
+ export { PencilSurface } from './PencilSurface.js';
20
+ export { PencilText } from './PencilText.js';
21
+ export { PencilIcon } from './PencilIcon.js';
22
+ export { createPencilPath, createPencilPoints } from './pencil.js';
@@ -0,0 +1,16 @@
1
+ import type { InputProps } from '../form/Input.js';
2
+ export interface InputNumberValueChangeDetails {
3
+ reason: 'input' | 'increment' | 'decrement';
4
+ }
5
+ export interface InputNumberProps extends Omit<InputProps, 'type' | 'value' | 'defaultValue' | 'onValueChange' | 'min' | 'max' | 'step' | 'startAction' | 'endAction' | 'clearable' | 'clearButtonLabel' | 'onClear'> {
6
+ value?: number | null;
7
+ defaultValue?: number | null;
8
+ onValueChange?: (value: number | null, details: InputNumberValueChangeDetails) => void;
9
+ min?: number;
10
+ max?: number;
11
+ step?: number | 'any';
12
+ controls?: boolean;
13
+ incrementButtonLabel?: string;
14
+ decrementButtonLabel?: string;
15
+ }
16
+ export declare const InputNumber: import("react").ForwardRefExoticComponent<InputNumberProps & import("react").RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
3
+ import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
4
+ import { Button } from '../Button.js';
5
+ import { PencilIcon } from '../PencilIcon.js';
6
+ import { Input } from '../form/Input.js';
7
+ import { useResolvedControlProps } from '../form/controlProps.js';
8
+ import { Tooltip } from '../tooltip/Tooltip.js';
9
+ // Lucide Plus / Minus ISC geometry uses the shared pencil renderer.
10
+ const plusPaths = [{ d: 'M5 12h14' }, { d: 'M12 5v14' }];
11
+ const minusPaths = [{ d: 'M5 12h14' }];
12
+ const finiteValue = (value) => typeof value === 'number' && Number.isFinite(value) ? value : null;
13
+ const format = (value) => String(finiteValue(value) ?? '');
14
+ const numberFromDraft = (draft) => draft === '' ? null : finiteValue(Number(draft));
15
+ export const InputNumber = forwardRef(function InputNumber({ classNames, controls = true, decrementButtonLabel = 'Decrease value', defaultValue, disabled, incrementButtonLabel = 'Increase value', inputMode = 'decimal', max, min, onChange, onValueChange, readOnly, size = 'md', step = 1, textAlign = 'center', value, ...props }, forwardedRef) {
16
+ const inputRef = useRef(null);
17
+ const isControlled = value !== undefined;
18
+ const [draft, setDraft] = useState(() => format(isControlled ? value : defaultValue));
19
+ const currentValue = numberFromDraft(draft);
20
+ const state = useResolvedControlProps({ disabled, readOnly });
21
+ const latest = useRef({ isControlled, value, defaultValue });
22
+ latest.current = { isControlled, value, defaultValue };
23
+ useImperativeHandle(forwardedRef, () => inputRef.current);
24
+ useBrowserLayoutEffect(() => {
25
+ if (isControlled && currentValue !== finiteValue(value))
26
+ setDraft(format(value));
27
+ }, [currentValue, draft, isControlled, value]);
28
+ useEffect(() => {
29
+ const input = inputRef.current;
30
+ const form = input?.form;
31
+ if (!input || !form)
32
+ return;
33
+ let resetTimer;
34
+ const reset = (event) => {
35
+ clearTimeout(resetTimer);
36
+ // Native reset runs after the event; restore the public default, not the latest draft.
37
+ resetTimer = setTimeout(() => {
38
+ if (event.defaultPrevented)
39
+ return;
40
+ const state = latest.current;
41
+ const restored = format(state.isControlled ? state.value : state.defaultValue);
42
+ input.value = restored;
43
+ setDraft(restored);
44
+ }, 0);
45
+ };
46
+ form.addEventListener('reset', reset);
47
+ return () => { clearTimeout(resetTimer); form.removeEventListener('reset', reset); };
48
+ }, [props.form]);
49
+ const cannotStep = state.disabled || state.readOnly || step === 'any' || (min !== undefined && max !== undefined && min > max);
50
+ const decreaseDisabled = cannotStep || (currentValue !== null && min !== undefined && currentValue <= min);
51
+ const increaseDisabled = cannotStep || (currentValue !== null && max !== undefined && currentValue >= max);
52
+ function changeStep(direction) {
53
+ const input = inputRef.current;
54
+ if (!input || input.matches(':disabled') || input.readOnly || cannotStep)
55
+ return;
56
+ const before = input.value;
57
+ // Native stepping applies the step base and decimal rounding without a custom parser.
58
+ if (direction === 'increment')
59
+ input.stepUp();
60
+ else
61
+ input.stepDown();
62
+ input.focus({ preventScroll: true });
63
+ if (input.value === before)
64
+ return;
65
+ setDraft(input.value);
66
+ onValueChange?.(Number.isFinite(input.valueAsNumber) ? input.valueAsNumber : null, { reason: direction });
67
+ }
68
+ const iconSize = size === 'sm' ? 18 : size === 'lg' ? 26 : 22;
69
+ const buttonSize = size === 'sm' ? 'icon-sm' : size === 'lg' ? 'icon-lg' : 'icon';
70
+ return _jsx(Input, { ...props, ref: inputRef, type: "number", inputMode: inputMode, textAlign: textAlign, clearable: false, size: size, min: min, max: max, step: step, disabled: state.disabled, readOnly: state.readOnly, value: draft, classNames: { ...classNames, root: ['inkra-input-number', classNames?.root].filter(Boolean).join(' ') }, onChange: event => {
71
+ const input = event.currentTarget;
72
+ const nextValue = Number.isFinite(input.valueAsNumber) ? input.valueAsNumber : null;
73
+ setDraft(input.value);
74
+ onChange?.(event);
75
+ onValueChange?.(nextValue, { reason: 'input' });
76
+ }, startAction: controls ? _jsx(Tooltip.Provider, { children: _jsx("span", { className: "inkra-input-number__controls", children: _jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { asChild: true, children: _jsx(Button, { type: "button", size: buttonSize, variant: "ghost", disabled: decreaseDisabled, "aria-label": decrementButtonLabel, onPointerDown: event => event.preventDefault(), onClick: () => changeStep('decrement'), children: _jsx(PencilIcon, { paths: minusPaths, size: iconSize, sourceSize: 24 }) }) }), _jsx(Tooltip.Portal, { children: _jsx(Tooltip.Content, { children: decrementButtonLabel }) })] }) }) }) : undefined, endAction: controls ? _jsx(Tooltip.Provider, { children: _jsx("span", { className: "inkra-input-number__controls", children: _jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { asChild: true, children: _jsx(Button, { type: "button", size: buttonSize, variant: "ghost", disabled: increaseDisabled, "aria-label": incrementButtonLabel, onPointerDown: event => event.preventDefault(), onClick: () => changeStep('increment'), children: _jsx(PencilIcon, { paths: plusPaths, size: iconSize, sourceSize: 24 }) }) }), _jsx(Tooltip.Portal, { children: _jsx(Tooltip.Content, { children: incrementButtonLabel }) })] }) }) }) : undefined });
77
+ });
@@ -0,0 +1 @@
1
+ export declare const InputActionContext: import("react").Context<boolean>;
@@ -0,0 +1,3 @@
1
+ import { createContext } from 'react';
2
+ // Input owns the shared outline and selection wash; the button supplies foreground.
3
+ export const InputActionContext = createContext(false);
@@ -0,0 +1,14 @@
1
+ export declare const PENCIL_RENDERER_VERSION = "inkra-pencil-v4";
2
+ export declare class BoundedCache<Value> {
3
+ private readonly maxEntries;
4
+ private readonly maxCost;
5
+ private readonly entries;
6
+ private totalCost;
7
+ constructor(maxEntries: number, maxCost: number);
8
+ get(key: string): Value | undefined;
9
+ set(key: string, value: Value, cost: number): Value;
10
+ delete(key: string): void;
11
+ }
12
+ export declare function canvasByteCost(canvas: HTMLCanvasElement): number;
13
+ export declare function canvasIsUsable(canvas: HTMLCanvasElement): boolean;
14
+ export declare function effectiveDpr(limit?: number): number;
@@ -0,0 +1,54 @@
1
+ export const PENCIL_RENDERER_VERSION = 'inkra-pencil-v4';
2
+ export class BoundedCache {
3
+ constructor(maxEntries, maxCost) {
4
+ this.maxEntries = maxEntries;
5
+ this.maxCost = maxCost;
6
+ this.entries = new Map();
7
+ this.totalCost = 0;
8
+ }
9
+ get(key) {
10
+ const entry = this.entries.get(key);
11
+ if (!entry)
12
+ return undefined;
13
+ this.entries.delete(key);
14
+ this.entries.set(key, entry);
15
+ return entry.value;
16
+ }
17
+ set(key, value, cost) {
18
+ const previous = this.entries.get(key);
19
+ if (previous) {
20
+ this.totalCost -= previous.cost;
21
+ this.entries.delete(key);
22
+ }
23
+ this.entries.set(key, { value, cost });
24
+ this.totalCost += cost;
25
+ while (this.entries.size > this.maxEntries || this.totalCost > this.maxCost) {
26
+ const oldestKey = this.entries.keys().next().value;
27
+ if (!oldestKey)
28
+ break;
29
+ const oldest = this.entries.get(oldestKey);
30
+ this.entries.delete(oldestKey);
31
+ this.totalCost -= oldest?.cost ?? 0;
32
+ }
33
+ return value;
34
+ }
35
+ delete(key) {
36
+ const entry = this.entries.get(key);
37
+ if (!entry)
38
+ return;
39
+ this.entries.delete(key);
40
+ this.totalCost -= entry.cost;
41
+ }
42
+ }
43
+ export function canvasByteCost(canvas) {
44
+ return canvas.width * canvas.height * 4;
45
+ }
46
+ export function canvasIsUsable(canvas) {
47
+ const context = canvas.getContext('2d');
48
+ return Boolean(context && !context.isContextLost?.());
49
+ }
50
+ export function effectiveDpr(limit = 2) {
51
+ if (typeof window === 'undefined')
52
+ return 1;
53
+ return Math.max(1, Math.min(window.devicePixelRatio || 1, limit));
54
+ }
@@ -0,0 +1,4 @@
1
+ import type { PencilPoint } from './stroke.js';
2
+ export type PencilLineOrientation = 'horizontal' | 'vertical';
3
+ export declare function createPencilLinePoints(width: number, height: number, orientation: PencilLineOrientation, roughness: number): readonly PencilPoint[];
4
+ export declare function pencilLineHasDrawableSize(width: number, height: number, orientation: PencilLineOrientation): boolean;
@@ -0,0 +1,21 @@
1
+ export function createPencilLinePoints(width, height, orientation, roughness) {
2
+ if (orientation === 'vertical') {
3
+ const middle = width / 2;
4
+ return [
5
+ [middle, 2],
6
+ [middle - roughness * 0.45, height * 0.34],
7
+ [middle + roughness * 0.3, height * 0.68],
8
+ [middle, height - 2],
9
+ ];
10
+ }
11
+ const middle = height / 2;
12
+ return [
13
+ [2, middle],
14
+ [width * 0.34, middle - roughness * 0.45],
15
+ [width * 0.68, middle + roughness * 0.3],
16
+ [width - 2, middle],
17
+ ];
18
+ }
19
+ export function pencilLineHasDrawableSize(width, height, orientation) {
20
+ return orientation === 'vertical' ? height >= 4 && width >= 2 : width >= 4 && height >= 2;
21
+ }
@@ -0,0 +1 @@
1
+ export declare function pencilPaper(color?: string, dark?: boolean, dpr?: number): HTMLCanvasElement;
@@ -0,0 +1,65 @@
1
+ import { BoundedCache, PENCIL_RENDERER_VERSION, canvasByteCost, canvasIsUsable } from './cache.js';
2
+ import { grain, seededRandom } from './random.js';
3
+ const PAPER_SIZE = 384;
4
+ const paperCache = new BoundedCache(8, 24 * 1024 * 1024);
5
+ export function pencilPaper(color = '#fcfcfa', dark = false, dpr = 1) {
6
+ const ratio = Math.max(1, Math.min(dpr, 2));
7
+ const key = JSON.stringify([PENCIL_RENDERER_VERSION, PAPER_SIZE, color, dark, ratio]);
8
+ const cached = paperCache.get(key);
9
+ if (cached && canvasIsUsable(cached))
10
+ return cached;
11
+ if (cached)
12
+ paperCache.delete(key);
13
+ const canvas = document.createElement('canvas');
14
+ canvas.width = Math.round(PAPER_SIZE * ratio);
15
+ canvas.height = Math.round(PAPER_SIZE * ratio);
16
+ const context = canvas.getContext('2d');
17
+ if (!context)
18
+ return canvas;
19
+ context.scale(ratio, ratio);
20
+ context.fillStyle = color;
21
+ context.fillRect(0, 0, PAPER_SIZE, PAPER_SIZE);
22
+ if (dark) {
23
+ grain(context, PAPER_SIZE, PAPER_SIZE, '#3b392f', 4331, 0.42);
24
+ grain(context, PAPER_SIZE, PAPER_SIZE, '#0a0a08', 7013, 0.3);
25
+ }
26
+ else {
27
+ grain(context, PAPER_SIZE, PAPER_SIZE, '#808278', 4331, 0.85);
28
+ grain(context, PAPER_SIZE, PAPER_SIZE, '#ffffff', 7013, 1.7);
29
+ }
30
+ const random = seededRandom(8119);
31
+ context.lineCap = 'round';
32
+ const fibreCount = dark ? 700 : 1100;
33
+ for (let index = 0; index < fibreCount; index += 1) {
34
+ const x = random() * PAPER_SIZE;
35
+ const y = random() * PAPER_SIZE;
36
+ const length = 2 + random() * 12;
37
+ const angle = random() * Math.PI;
38
+ const deltaX = Math.cos(angle) * length;
39
+ const deltaY = Math.sin(angle) * length * 0.45;
40
+ const bend = (random() - 0.5) * 2;
41
+ const alpha = dark ? 0.025 + random() * 0.025 : 0.025 + random() * 0.055;
42
+ context.lineWidth = 0.35 + random() * 0.45;
43
+ for (const offsetX of [-PAPER_SIZE, 0, PAPER_SIZE]) {
44
+ for (const offsetY of [-PAPER_SIZE, 0, PAPER_SIZE]) {
45
+ if (x + offsetX + length < 0 || x + offsetX - length > PAPER_SIZE
46
+ || y + offsetY + length < 0 || y + offsetY - length > PAPER_SIZE)
47
+ continue;
48
+ context.strokeStyle = dark ? '#0d0d0b' : '#82877b';
49
+ context.globalAlpha = alpha;
50
+ context.beginPath();
51
+ context.moveTo(x + offsetX, y + offsetY);
52
+ context.quadraticCurveTo(x + offsetX + deltaX / 2, y + offsetY + deltaY / 2 + bend, x + offsetX + deltaX, y + offsetY + deltaY);
53
+ context.stroke();
54
+ context.strokeStyle = dark ? '#494737' : '#ffffff';
55
+ context.globalAlpha = dark ? alpha * 1.2 : alpha * 2.5;
56
+ context.beginPath();
57
+ context.moveTo(x + offsetX, y + offsetY + 0.7);
58
+ context.quadraticCurveTo(x + offsetX + deltaX / 2, y + offsetY + deltaY / 2 + bend + 0.7, x + offsetX + deltaX, y + offsetY + deltaY + 0.7);
59
+ context.stroke();
60
+ }
61
+ }
62
+ }
63
+ context.globalAlpha = 1;
64
+ return paperCache.set(key, canvas, canvasByteCost(canvas));
65
+ }
@@ -0,0 +1,3 @@
1
+ export declare function seedToNumber(seed: string | number): number;
2
+ export declare function seededRandom(seed: string | number): () => number;
3
+ export declare function grain(context: CanvasRenderingContext2D, width: number, height: number, color: string, seed: string | number, amount: number): void;
@@ -0,0 +1,30 @@
1
+ export function seedToNumber(seed) {
2
+ if (typeof seed === 'number' && Number.isFinite(seed))
3
+ return seed >>> 0;
4
+ const value = String(seed);
5
+ let hash = 2166136261;
6
+ for (let index = 0; index < value.length; index += 1) {
7
+ hash ^= value.charCodeAt(index);
8
+ hash = Math.imul(hash, 16777619);
9
+ }
10
+ return hash >>> 0;
11
+ }
12
+ export function seededRandom(seed) {
13
+ let state = seedToNumber(seed) || 1;
14
+ return () => {
15
+ state += 0x6d2b79f5;
16
+ let value = Math.imul(state ^ (state >>> 15), state | 1);
17
+ value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
18
+ return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
19
+ };
20
+ }
21
+ export function grain(context, width, height, color, seed, amount) {
22
+ const random = seededRandom(seed);
23
+ context.save();
24
+ context.fillStyle = color;
25
+ for (let index = 0; index < width * height * 0.2; index += 1) {
26
+ context.globalAlpha = (0.02 + random() * 0.13) * amount;
27
+ context.fillRect(random() * width, random() * height, 0.5 + random() * 0.6, 0.4 + random() * 0.7);
28
+ }
29
+ context.restore();
30
+ }
@@ -0,0 +1 @@
1
+ export declare function observeCanvasRecovery(callback: () => void): () => void;
@@ -0,0 +1,52 @@
1
+ const subscribers = new Set();
2
+ let frame = 0;
3
+ function recover() {
4
+ frame = 0;
5
+ if (document.visibilityState === 'hidden')
6
+ return;
7
+ for (const callback of subscribers) {
8
+ try {
9
+ callback();
10
+ }
11
+ catch {
12
+ // One failed canvas must not block independent painters.
13
+ }
14
+ }
15
+ }
16
+ function scheduleRecovery() {
17
+ if (!frame && document.visibilityState !== 'hidden')
18
+ frame = requestAnimationFrame(recover);
19
+ }
20
+ function onVisibility() {
21
+ if (document.visibilityState === 'hidden') {
22
+ cancelAnimationFrame(frame);
23
+ frame = 0;
24
+ }
25
+ else {
26
+ scheduleRecovery();
27
+ }
28
+ }
29
+ function onPageShow(event) {
30
+ if (event.persisted)
31
+ scheduleRecovery();
32
+ }
33
+ export function observeCanvasRecovery(callback) {
34
+ if (!subscribers.size) {
35
+ document.addEventListener('visibilitychange', onVisibility);
36
+ document.addEventListener('resume', scheduleRecovery);
37
+ document.addEventListener('contextrestored', scheduleRecovery, true);
38
+ window.addEventListener('pageshow', onPageShow);
39
+ }
40
+ subscribers.add(callback);
41
+ return () => {
42
+ subscribers.delete(callback);
43
+ if (subscribers.size)
44
+ return;
45
+ document.removeEventListener('visibilitychange', onVisibility);
46
+ document.removeEventListener('resume', scheduleRecovery);
47
+ document.removeEventListener('contextrestored', scheduleRecovery, true);
48
+ window.removeEventListener('pageshow', onPageShow);
49
+ cancelAnimationFrame(frame);
50
+ frame = 0;
51
+ };
52
+ }
@@ -0,0 +1,24 @@
1
+ export type PencilPoint = readonly [x: number, y: number];
2
+ export interface StrokeSettings {
3
+ variation?: number;
4
+ breaks?: number;
5
+ grain?: number;
6
+ taperLength?: number;
7
+ scale?: number;
8
+ distanceOffset?: number;
9
+ totalDistance?: number;
10
+ step?: number;
11
+ detail?: 'fill';
12
+ viewport?: readonly [width: number, height: number];
13
+ viewportTop?: number;
14
+ filaments?: number;
15
+ gain?: number;
16
+ edgeGrain?: boolean;
17
+ }
18
+ export declare function strokeProfile(at: number, total: number, seed: number, settings?: StrokeSettings): {
19
+ pressure: number;
20
+ contact: number;
21
+ deposit: number;
22
+ taper: number;
23
+ };
24
+ export declare function pencilStroke(context: CanvasRenderingContext2D, points: readonly PencilPoint[], color: string, width: number, seed: number, amplitude: number, passes?: number, closed?: boolean, settings?: StrokeSettings): void;