diff --git a/components/canvas/cursor.tsx b/components/canvas/cursor.tsx index 8b1eccbaf..4500bf675 100644 --- a/components/canvas/cursor.tsx +++ b/components/canvas/cursor.tsx @@ -6,7 +6,6 @@ export default function Cursor() { useEffect(() => { function updatePosition(e: PointerEvent) { - console.log('hi') const cursor = rCursor.current cursor.setAttribute( diff --git a/components/code-panel/code-panel.tsx b/components/code-panel/code-panel.tsx index 8247b4680..1f4dde011 100644 --- a/components/code-panel/code-panel.tsx +++ b/components/code-panel/code-panel.tsx @@ -82,7 +82,7 @@ export default function CodePanel() { let error = null try { - const { shapes, controls } = generateFromCode(data.code) + const { shapes, controls } = generateFromCode(state.data, data.code) state.send('GENERATED_FROM_CODE', { shapes, controls }) } catch (e) { console.error(e) diff --git a/lib/code/circle.ts b/lib/code/circle.ts index eef34feab..feb16c609 100644 --- a/lib/code/circle.ts +++ b/lib/code/circle.ts @@ -11,10 +11,10 @@ export default class Circle extends CodeShape { super({ id: uuid(), seed: Math.random(), + parentId: (window as any).currentPageId, type: ShapeType.Circle, isGenerated: true, name: 'Circle', - parentId: 'page0', childIndex: 0, point: [0, 0], rotation: 0, @@ -22,8 +22,8 @@ export default class Circle extends CodeShape { isAspectRatioLocked: false, isLocked: false, isHidden: false, - style: defaultStyle, ...props, + style: { ...defaultStyle, ...props.style }, }) } diff --git a/lib/code/dot.ts b/lib/code/dot.ts index fb52c880c..6340a0f27 100644 --- a/lib/code/dot.ts +++ b/lib/code/dot.ts @@ -11,10 +11,10 @@ export default class Dot extends CodeShape { super({ id: uuid(), seed: Math.random(), + parentId: (window as any).currentPageId, type: ShapeType.Dot, isGenerated: true, name: 'Dot', - parentId: 'page0', childIndex: 0, point: [0, 0], rotation: 0, @@ -25,7 +25,7 @@ export default class Dot extends CodeShape { style: { ...defaultStyle, ...props.style, - isFilled: false, + isFilled: true, }, }) } diff --git a/lib/code/ellipse.ts b/lib/code/ellipse.ts index b51ca5ea5..89445b61a 100644 --- a/lib/code/ellipse.ts +++ b/lib/code/ellipse.ts @@ -11,10 +11,10 @@ export default class Ellipse extends CodeShape { super({ id: uuid(), seed: Math.random(), + parentId: (window as any).currentPageId, type: ShapeType.Ellipse, isGenerated: true, name: 'Ellipse', - parentId: 'page0', childIndex: 0, point: [0, 0], radiusX: 20, @@ -23,8 +23,8 @@ export default class Ellipse extends CodeShape { isAspectRatioLocked: false, isLocked: false, isHidden: false, - style: defaultStyle, ...props, + style: { ...defaultStyle, ...props.style }, }) } diff --git a/lib/code/generate.ts b/lib/code/generate.ts index f6bd0b08e..e094a9338 100644 --- a/lib/code/generate.ts +++ b/lib/code/generate.ts @@ -1,15 +1,15 @@ -import Rectangle from "./rectangle" -import Circle from "./circle" -import Ellipse from "./ellipse" -import Polyline from "./polyline" -import Dot from "./dot" -import Ray from "./ray" -import Line from "./line" -import Vector from "./vector" -import Utils from "./utils" -import { NumberControl, VectorControl, codeControls, controls } from "./control" -import { codeShapes } from "./index" -import { CodeControl } from "types" +import Rectangle from './rectangle' +import Circle from './circle' +import Ellipse from './ellipse' +import Polyline from './polyline' +import Dot from './dot' +import Ray from './ray' +import Line from './line' +import Vector from './vector' +import Utils from './utils' +import { NumberControl, VectorControl, codeControls, controls } from './control' +import { codeShapes } from './index' +import { CodeControl, Data } from 'types' const baseScope = { Dot, @@ -30,12 +30,14 @@ const baseScope = { * collected shapes as an array. * @param code */ -export function generateFromCode(code: string) { +export function generateFromCode(data: Data, code: string) { codeControls.clear() codeShapes.clear() ;(window as any).isUpdatingCode = false + ;(window as any).currentPageId = data.currentPageId - const scope = { ...baseScope, controls } + const { currentPageId } = data + const scope = { ...baseScope, controls, currentPageId } new Function(...Object.keys(scope), `${code}`)(...Object.values(scope)) @@ -53,15 +55,16 @@ export function generateFromCode(code: string) { * collected shapes as an array. * @param code */ -export function updateFromCode( - code: string, - controls: Record -) { +export function updateFromCode(data: Data, code: string) { codeShapes.clear() ;(window as any).isUpdatingCode = true + ;(window as any).currentPageId = data.currentPageId + + const { currentPageId } = data const scope = { ...baseScope, + currentPageId, controls: Object.fromEntries( Object.entries(controls).map(([id, control]) => [ control.label, diff --git a/lib/code/line.ts b/lib/code/line.ts index 16b293d71..12d7f1a1c 100644 --- a/lib/code/line.ts +++ b/lib/code/line.ts @@ -12,10 +12,10 @@ export default class Line extends CodeShape { super({ id: uuid(), seed: Math.random(), + parentId: (window as any).currentPageId, type: ShapeType.Line, isGenerated: true, name: 'Line', - parentId: 'page0', childIndex: 0, point: [0, 0], direction: [-0.5, 0.5], diff --git a/lib/code/polyline.ts b/lib/code/polyline.ts index 7f5910148..9ad64fbd7 100644 --- a/lib/code/polyline.ts +++ b/lib/code/polyline.ts @@ -12,10 +12,10 @@ export default class Polyline extends CodeShape { super({ id: uuid(), seed: Math.random(), + parentId: (window as any).currentPageId, type: ShapeType.Polyline, isGenerated: true, name: 'Polyline', - parentId: 'page0', childIndex: 0, point: [0, 0], points: [[0, 0]], diff --git a/lib/code/rectangle.ts b/lib/code/rectangle.ts index 0b82a026f..feaa47bd2 100644 --- a/lib/code/rectangle.ts +++ b/lib/code/rectangle.ts @@ -12,10 +12,10 @@ export default class Rectangle extends CodeShape { super({ id: uuid(), seed: Math.random(), + parentId: (window as any).currentPageId, type: ShapeType.Rectangle, isGenerated: true, name: 'Rectangle', - parentId: 'page0', childIndex: 0, point: [0, 0], size: [100, 100], @@ -24,8 +24,8 @@ export default class Rectangle extends CodeShape { isAspectRatioLocked: false, isLocked: false, isHidden: false, - style: defaultStyle, ...props, + style: { ...defaultStyle, ...props.style }, }) } diff --git a/lib/code/utils.ts b/lib/code/utils.ts index 30dd01fca..267e1934c 100644 --- a/lib/code/utils.ts +++ b/lib/code/utils.ts @@ -1,5 +1,5 @@ -import { Bounds } from "types" -import Vector, { Point } from "./vector" +import { Bounds } from 'types' +import Vector, { Point } from './vector' export default class Utils { static getRayRayIntersection(p0: Vector, n0: Vector, p1: Vector, n1: Vector) { diff --git a/lib/code/vector.ts b/lib/code/vector.ts index f82f1a131..26ba0fc12 100644 --- a/lib/code/vector.ts +++ b/lib/code/vector.ts @@ -16,7 +16,7 @@ export default class Vector { constructor(vector: Vector, b?: undefined) constructor(options: Point, b?: undefined) constructor(a: VectorOptions | Vector | number, b?: number) { - if (typeof a === "number") { + if (typeof a === 'number') { this.x = a this.y = b } else { @@ -415,7 +415,7 @@ export default class Vector { } static cast(v: Point | Vector) { - return "cast" in v ? v : new Vector(v) + return 'cast' in v ? v : new Vector(v) } static from(v: Vector) { diff --git a/lib/shape-utils/dot.tsx b/lib/shape-utils/dot.tsx index 1e4a7c74b..95bee2267 100644 --- a/lib/shape-utils/dot.tsx +++ b/lib/shape-utils/dot.tsx @@ -34,7 +34,7 @@ const dot = registerShapeUtils({ }, render({ id }) { - return + return }, getBounds(shape) { diff --git a/lib/shape-utils/ellipse.tsx b/lib/shape-utils/ellipse.tsx index f703fd920..fd933bf69 100644 --- a/lib/shape-utils/ellipse.tsx +++ b/lib/shape-utils/ellipse.tsx @@ -1,17 +1,25 @@ import { v4 as uuid } from 'uuid' import * as vec from 'utils/vec' import { EllipseShape, ShapeType } from 'types' -import { registerShapeUtils } from './index' +import { getShapeUtils, registerShapeUtils } from './index' import { boundsContained, getRotatedEllipseBounds } from 'utils/bounds' import { intersectEllipseBounds } from 'utils/intersections' import { pointInEllipse } from 'utils/hitTests' import { + ease, getBoundsFromPoints, getRotatedCorners, + getSvgPathFromStroke, + pointsBetween, + rng, rotateBounds, + shuffleArr, translateBounds, } from 'utils/utils' import { defaultStyle, getShapeStyle } from 'lib/shape-styles' +import getStroke from 'perfect-freehand' + +const pathCache = new WeakMap([]) const ellipse = registerShapeUtils({ boundsCache: new WeakMap([]), @@ -37,17 +45,39 @@ const ellipse = registerShapeUtils({ } }, - render({ id, radiusX, radiusY, style }) { + render(shape) { + const { id, radiusX, radiusY, style } = shape const styles = getShapeStyle(style) + + if (!pathCache.has(shape)) { + renderPath(shape) + } + + const path = pathCache.get(shape) + return ( - + + + + ) + + // return ( + // + // ) }, getBounds(shape) { @@ -129,3 +159,53 @@ const ellipse = registerShapeUtils({ }) export default ellipse + +function renderPath(shape: EllipseShape) { + const { style, id, radiusX, radiusY, point } = shape + + const getRandom = rng(id) + + const center = vec.sub(getShapeUtils(shape).getCenter(shape), point) + + const strokeWidth = +getShapeStyle(style).strokeWidth + + const rx = radiusX + getRandom() * strokeWidth + const ry = radiusY + getRandom() * strokeWidth + + const points: number[][] = [] + const start = Math.PI + Math.PI * getRandom() + + const overlap = Math.PI / 12 + + for (let i = 2; i < 8; i++) { + const rads = start + overlap * 2 * (i / 8) + const x = rx * Math.cos(rads) + center[0] + const y = ry * Math.sin(rads) + center[1] + points.push([x, y]) + } + + for (let i = 5; i < 32; i++) { + const rads = start + overlap * 2 + Math.PI * 2.5 * ease(i / 35) + const x = rx * Math.cos(rads) + center[0] + const y = ry * Math.sin(rads) + center[1] + points.push([x, y]) + } + + for (let i = 0; i < 8; i++) { + const rads = start + overlap * 2 * (i / 4) + const x = rx * Math.cos(rads) + center[0] + const y = ry * Math.sin(rads) + center[1] + points.push([x, y]) + } + + const stroke = getStroke(points, { + size: 1 + strokeWidth * 2, + thinning: 0.6, + easing: (t) => t * t * t * t, + end: { taper: strokeWidth * 20 }, + start: { taper: strokeWidth * 20 }, + simulatePressure: false, + }) + + pathCache.set(shape, getSvgPathFromStroke(stroke)) +} diff --git a/lib/shape-utils/rectangle.tsx b/lib/shape-utils/rectangle.tsx index 649003911..f2c65f2d1 100644 --- a/lib/shape-utils/rectangle.tsx +++ b/lib/shape-utils/rectangle.tsx @@ -2,7 +2,13 @@ import { v4 as uuid } from 'uuid' import * as vec from 'utils/vec' import { RectangleShape, ShapeType } from 'types' import { registerShapeUtils } from './index' -import { getSvgPathFromStroke, translateBounds, getNoise } from 'utils/utils' +import { + getSvgPathFromStroke, + translateBounds, + rng, + shuffleArr, + pointsBetween, +} from 'utils/utils' import { defaultStyle, getShapeStyle } from 'lib/shape-styles' import getStroke from 'perfect-freehand' @@ -33,7 +39,7 @@ const rectangle = registerShapeUtils({ }, render(shape) { - const { id, size, radius, style, point } = shape + const { id, size, radius, style } = shape const styles = getShapeStyle(style) if (!pathCache.has(shape)) { @@ -117,29 +123,16 @@ const rectangle = registerShapeUtils({ export default rectangle -function easeInOut(t: number) { - return t * (2 - t) -} - -function ease(t: number) { - return t * t * t -} - -function pointsBetween(a: number[], b: number[], steps = 6) { - return Array.from(Array(steps)) - .map((_, i) => ease(i / steps)) - .map((t) => [...vec.lrp(a, b, t), (1 - t) / 2]) -} - function renderPath(shape: RectangleShape) { const styles = getShapeStyle(shape.style) - const noise = getNoise(shape.seed) - const off = -0.25 + shape.seed / 2 + const getRandom = rng(shape.id) + + const baseOffset = +styles.strokeWidth / 2 const offsets = Array.from(Array(4)).map((_, i) => [ - noise(i, i + 1) * off * 16, - noise(i + 2, i + 3) * off * 16, + getRandom() * baseOffset, + getRandom() * baseOffset, ]) const [w, h] = shape.size @@ -155,17 +148,11 @@ function renderPath(shape: RectangleShape) { pointsBetween(bl, tl), pointsBetween(tl, tr), ], - shape.id.charCodeAt(5) + Math.floor(5 + getRandom() * 4) ) const stroke = getStroke( - [ - ...lines.flat().slice(4), - ...lines[0].slice(0, 4), - lines[0][4], - lines[0][5], - lines[0][5], - ], + [...lines.flat().slice(2), ...lines[0], ...lines[0].slice(4)], { size: 1 + +styles.strokeWidth * 2, thinning: 0.6, @@ -178,7 +165,3 @@ function renderPath(shape: RectangleShape) { pathCache.set(shape, getSvgPathFromStroke(stroke)) } - -function shuffleArr(arr: T[], offset: number): T[] { - return arr.map((_, i) => arr[(i + offset) % arr.length]) -} diff --git a/state/sessions/translate-session.ts b/state/sessions/translate-session.ts index ef8be9ecf..db973cf07 100644 --- a/state/sessions/translate-session.ts +++ b/state/sessions/translate-session.ts @@ -33,9 +33,6 @@ export default class TranslateSession extends BaseSession { const { shapes } = getPage(data, currentPageId) const delta = vec.vec(this.origin, point) - const trueDelta = vec.sub(delta, this.prev) - this.delta = delta - this.prev = delta if (isAligned) { if (Math.abs(delta[0]) < Math.abs(delta[1])) { @@ -45,13 +42,17 @@ export default class TranslateSession extends BaseSession { } } + const trueDelta = vec.sub(delta, this.prev) + this.delta = delta + this.prev = delta + if (isCloning) { if (!this.isCloning) { this.isCloning = true - for (const { id, point } of initialShapes) { + for (const { id } of initialShapes) { const shape = shapes[id] - getShapeUtils(shape).translateTo(shape, point) + getShapeUtils(shape).translateBy(shape, trueDelta) } for (const clone of clones) { @@ -70,9 +71,9 @@ export default class TranslateSession extends BaseSession { ) } - for (const { id, point } of clones) { + for (const { id } of clones) { const shape = shapes[id] - getShapeUtils(shape).translateTo(shape, vec.add(point, delta)) + getShapeUtils(shape).translateBy(shape, trueDelta) } updateParents( @@ -186,6 +187,7 @@ export function getTranslateSnapshot(data: Data) { clones: selectedShapes .filter((shape) => shape.type !== ShapeType.Group) .flatMap((shape) => { + // TODO: Clone children recursively const clone = { ...shape, id: uuid(), diff --git a/state/state.ts b/state/state.ts index 41d61bcd2..4c6df8700 100644 --- a/state/state.ts +++ b/state/state.ts @@ -1382,8 +1382,8 @@ const state = createState({ try { const { shapes } = updateFromCode( - data.document.code[data.currentCodeFileId].code, - data.codeControls + data, + data.document.code[data.currentCodeFileId].code ) commands.generate(data, data.currentPageId, shapes) diff --git a/utils/utils.ts b/utils/utils.ts index 4f1a9c982..bfa642898 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -1695,68 +1695,45 @@ const Grad = [ [0, -1], ] -// Thanks to joshforisha -// https://github.com/joshforisha/fast-simplex-noise-js/blob/main/src/2d.ts -export function getNoise(seed = Math.random()) { - const p = new Uint8Array(256) - for (let i = 0; i < 256; i++) p[i] = i +/** + * Seeded random number generator, using [xorshift](https://en.wikipedia.org/wiki/Xorshift). + * The result will always be betweeen -1 and 1. + * + * Adapted from [seedrandom](https://github.com/davidbau/seedrandom). + */ +export function rng(seed = '') { + let x = 0 + let y = 0 + let z = 0 + let w = 0 - let n: number - let q: number - for (let i = 255; i > 0; i--) { - n = Math.floor((i + 1) * seed) - q = p[i] - p[i] = p[n] - p[n] = q + function next() { + const t = x ^ (x << 11) + x = y + y = z + z = w + w ^= ((w >>> 19) ^ t ^ (t >>> 8)) >>> 0 + return w / 0x100000000 } - const perm = new Uint8Array(512) - const permMod12 = new Uint8Array(512) - for (let i = 0; i < 512; i++) { - perm[i] = p[i & 255] - permMod12[i] = perm[i] % 12 + for (var k = 0; k < seed.length + 64; k++) { + x ^= seed.charCodeAt(k) | 0 + next() } - return (x: number, y: number): number => { - // Skew the input space to determine which simplex cell we're in - const s = (x + y) * 0.5 * (Math.sqrt(3.0) - 1.0) // Hairy factor for 2D - const i = Math.floor(x + s) - const j = Math.floor(y + s) - const t = (i + j) * G2 - const X0 = i - t // Unskew the cell origin back to (x,y) space - const Y0 = j - t - const x0 = x - X0 // The x,y distances from the cell origin - const y0 = y - Y0 - - // Determine which simplex we are in. - const i1 = x0 > y0 ? 1 : 0 - const j1 = x0 > y0 ? 0 : 1 - - // Offsets for corners - const x1 = x0 - i1 + G2 - const y1 = y0 - j1 + G2 - const x2 = x0 - 1.0 + 2.0 * G2 - const y2 = y0 - 1.0 + 2.0 * G2 - - // Work out the hashed gradient indices of the three simplex corners - const ii = i & 255 - const jj = j & 255 - const g0 = Grad[permMod12[ii + perm[jj]]] - const g1 = Grad[permMod12[ii + i1 + perm[jj + j1]]] - const g2 = Grad[permMod12[ii + 1 + perm[jj + 1]]] - - // Calculate the contribution from the three corners - const t0 = 0.5 - x0 * x0 - y0 * y0 - const n0 = t0 < 0 ? 0.0 : Math.pow(t0, 4) * (g0[0] * x0 + g0[1] * y0) - - const t1 = 0.5 - x1 * x1 - y1 * y1 - const n1 = t1 < 0 ? 0.0 : Math.pow(t1, 4) * (g1[0] * x1 + g1[1] * y1) - - const t2 = 0.5 - x2 * x2 - y2 * y2 - const n2 = t2 < 0 ? 0.0 : Math.pow(t2, 4) * (g2[0] * x2 + g2[1] * y2) - - // Add contributions from each corner to get the final noise value. - // The result is scaled to return values in the interval [-1, 1] - return 70.14805770653952 * (n0 + n1 + n2) - } + return next +} + +export function ease(t: number) { + return t * t * t +} + +export function pointsBetween(a: number[], b: number[], steps = 6) { + return Array.from(Array(steps)) + .map((_, i) => ease(i / steps)) + .map((t) => [...vec.lrp(a, b, t), (1 - t) / 2]) +} + +export function shuffleArr(arr: T[], offset: number): T[] { + return arr.map((_, i) => arr[(i + offset) % arr.length]) }