fixes code bug, adds sketchy circle

canvas-rendering
Steve Ruiz 2021-06-08 11:32:20 +01:00
rodzic dd1210c617
commit 5a7e79121a
17 zmienionych plików z 188 dodań i 144 usunięć

Wyświetl plik

@ -6,7 +6,6 @@ export default function Cursor() {
useEffect(() => {
function updatePosition(e: PointerEvent) {
console.log('hi')
const cursor = rCursor.current
cursor.setAttribute(

Wyświetl plik

@ -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)

Wyświetl plik

@ -11,10 +11,10 @@ export default class Circle extends CodeShape<CircleShape> {
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<CircleShape> {
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: defaultStyle,
...props,
style: { ...defaultStyle, ...props.style },
})
}

Wyświetl plik

@ -11,10 +11,10 @@ export default class Dot extends CodeShape<DotShape> {
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<DotShape> {
style: {
...defaultStyle,
...props.style,
isFilled: false,
isFilled: true,
},
})
}

Wyświetl plik

@ -11,10 +11,10 @@ export default class Ellipse extends CodeShape<EllipseShape> {
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<EllipseShape> {
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: defaultStyle,
...props,
style: { ...defaultStyle, ...props.style },
})
}

Wyświetl plik

@ -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<string, CodeControl>
) {
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,

Wyświetl plik

@ -12,10 +12,10 @@ export default class Line extends CodeShape<LineShape> {
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],

Wyświetl plik

@ -12,10 +12,10 @@ export default class Polyline extends CodeShape<PolylineShape> {
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]],

Wyświetl plik

@ -12,10 +12,10 @@ export default class Rectangle extends CodeShape<RectangleShape> {
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<RectangleShape> {
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: defaultStyle,
...props,
style: { ...defaultStyle, ...props.style },
})
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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) {

Wyświetl plik

@ -34,7 +34,7 @@ const dot = registerShapeUtils<DotShape>({
},
render({ id }) {
return <use href="#dot" />
return <use id={id} href="#dot" fill="black" />
},
getBounds(shape) {

Wyświetl plik

@ -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<EllipseShape, string>([])
const ellipse = registerShapeUtils<EllipseShape>({
boundsCache: new WeakMap([]),
@ -37,17 +45,39 @@ const ellipse = registerShapeUtils<EllipseShape>({
}
},
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 (
<ellipse
id={id}
cx={radiusX}
cy={radiusY}
rx={Math.max(0, radiusX - Number(styles.strokeWidth) / 2)}
ry={Math.max(0, radiusY - Number(styles.strokeWidth) / 2)}
/>
<g id={id}>
<ellipse
id={id}
cx={radiusX}
cy={radiusY}
rx={Math.max(0, radiusX - Number(styles.strokeWidth) / 2)}
ry={Math.max(0, radiusY - Number(styles.strokeWidth) / 2)}
stroke="none"
/>
<path d={path} fill={styles.stroke} />
</g>
)
// return (
// <ellipse
// id={id}
// cx={radiusX}
// cy={radiusY}
// rx={Math.max(0, radiusX - Number(styles.strokeWidth) / 2)}
// ry={Math.max(0, radiusY - Number(styles.strokeWidth) / 2)}
// />
// )
},
getBounds(shape) {
@ -129,3 +159,53 @@ const ellipse = registerShapeUtils<EllipseShape>({
})
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))
}

Wyświetl plik

@ -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<RectangleShape>({
},
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<RectangleShape>({
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<T>(arr: T[], offset: number): T[] {
return arr.map((_, i) => arr[(i + offset) % arr.length])
}

Wyświetl plik

@ -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(),

Wyświetl plik

@ -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)

Wyświetl plik

@ -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<T>(arr: T[], offset: number): T[] {
return arr.map((_, i) => arr[(i + offset) % arr.length])
}