|
|
@@ -1,436 +0,0 @@
|
|
|
-import { OptimizedBuffer, RGBA, TextAttributes } from "@opentui/core"
|
|
|
-import { go } from "../logo"
|
|
|
-
|
|
|
-const PERIOD = 4600
|
|
|
-const RINGS = 3
|
|
|
-const WIDTH = 3.8
|
|
|
-const TAIL = 9.5
|
|
|
-const AMP = 0.55
|
|
|
-const TAIL_AMP = 0.16
|
|
|
-const BREATH_AMP = 0.05
|
|
|
-const BREATH_SPEED = 0.0008
|
|
|
-// Offset so the bg ring emits from the estimated GO center when the logo shimmer peaks.
|
|
|
-const PHASE_OFFSET = 0.29
|
|
|
-const LOGO_GAP = 1
|
|
|
-const LOGO_TOP_BIAS = -1
|
|
|
-const LOGO_LEFT_WIDTH = go.left[0]?.length ?? 0
|
|
|
-const LOGO_LINES = go.left.map((line, index) => line + " ".repeat(LOGO_GAP) + go.right[index])
|
|
|
-const LOGO_WIDTH = LOGO_LINES[0]?.length ?? 0
|
|
|
-const LOGO_HEIGHT = LOGO_LINES.length
|
|
|
-const SPACE = " ".codePointAt(0)!
|
|
|
-const TOP_HALF = "▀".codePointAt(0)!
|
|
|
-const FULL_BLOCK = "█".codePointAt(0)!
|
|
|
-const RING_SCALE = 1 / RINGS
|
|
|
-const TAIL_SCALE = 1 / TAIL
|
|
|
-const LOGO_REACH = Math.hypot(LOGO_WIDTH, LOGO_HEIGHT * 2) + 3
|
|
|
-
|
|
|
-const enum LogoCellKind {
|
|
|
- Background,
|
|
|
- Top,
|
|
|
- ShadowTop,
|
|
|
- Solid,
|
|
|
- Char,
|
|
|
-}
|
|
|
-
|
|
|
-type LogoTemplateCell = {
|
|
|
- x: number
|
|
|
- y: number
|
|
|
- kind: LogoCellKind
|
|
|
- charCode: number
|
|
|
- attributes: number
|
|
|
- topDist: number
|
|
|
- bottomDist: number
|
|
|
-}
|
|
|
-
|
|
|
-const LOGO_TEMPLATE: LogoTemplateCell[] = LOGO_LINES.flatMap((line, y) =>
|
|
|
- Array.from(line)
|
|
|
- .map((char, x) => {
|
|
|
- if (char === " ") return
|
|
|
- const kind =
|
|
|
- char === "_"
|
|
|
- ? LogoCellKind.Background
|
|
|
- : char === "^"
|
|
|
- ? LogoCellKind.Top
|
|
|
- : char === "~"
|
|
|
- ? LogoCellKind.ShadowTop
|
|
|
- : char === "█"
|
|
|
- ? LogoCellKind.Solid
|
|
|
- : LogoCellKind.Char
|
|
|
- return {
|
|
|
- x,
|
|
|
- y,
|
|
|
- kind,
|
|
|
- charCode: char.codePointAt(0) ?? SPACE,
|
|
|
- attributes: x > LOGO_LEFT_WIDTH ? TextAttributes.BOLD : 0,
|
|
|
- topDist: Math.hypot(x + 0.5 - LOGO_WIDTH / 2, y * 2 - LOGO_HEIGHT),
|
|
|
- bottomDist: Math.hypot(x + 0.5 - LOGO_WIDTH / 2, y * 2 + 1 - LOGO_HEIGHT),
|
|
|
- }
|
|
|
- })
|
|
|
- .filter((cell): cell is LogoTemplateCell => !!cell),
|
|
|
-)
|
|
|
-
|
|
|
-export type Rgb = [number, number, number]
|
|
|
-
|
|
|
-export type GoUpsellArtRenderOptions = {
|
|
|
- deltaTime?: number
|
|
|
- rgb?: boolean
|
|
|
- cache?: boolean
|
|
|
-}
|
|
|
-
|
|
|
-const CACHE_FRAME_COUNT = Math.round(PERIOD / (1000 / 30))
|
|
|
-const CACHE_FRAMES_PER_RENDER = 1
|
|
|
-
|
|
|
-export function toRgb(color: RGBA): Rgb {
|
|
|
- const [r, g, b] = color.toInts()
|
|
|
- return [r, g, b]
|
|
|
-}
|
|
|
-
|
|
|
-function clamp(n: number) {
|
|
|
- return Math.max(0, Math.min(1, n))
|
|
|
-}
|
|
|
-
|
|
|
-function writeRgb(buffer: Uint16Array, offset: number, r: number, g: number, b: number, a = 255) {
|
|
|
- buffer[offset] = r
|
|
|
- buffer[offset + 1] = g
|
|
|
- buffer[offset + 2] = b
|
|
|
- buffer[offset + 3] = a
|
|
|
-}
|
|
|
-
|
|
|
-function mixChannel(base: number, overlay: number, alpha: number) {
|
|
|
- return Math.round(base + (overlay - base) * clamp(alpha))
|
|
|
-}
|
|
|
-
|
|
|
-function writeLogoTint(
|
|
|
- buffer: Uint16Array,
|
|
|
- offset: number,
|
|
|
- base: Rgb,
|
|
|
- primary: Rgb,
|
|
|
- primaryMix: number,
|
|
|
- peakMix: number,
|
|
|
-) {
|
|
|
- const p = clamp(primaryMix)
|
|
|
- const q = clamp(peakMix)
|
|
|
- const r = mixChannel(mixChannel(base[0], primary[0], p), 255, q)
|
|
|
- const g = mixChannel(mixChannel(base[1], primary[1], p), 255, q)
|
|
|
- const b = mixChannel(mixChannel(base[2], primary[2], p), 255, q)
|
|
|
- writeRgb(buffer, offset, r, g, b)
|
|
|
-}
|
|
|
-
|
|
|
-function sameRgb(a: Rgb, b: Rgb) {
|
|
|
- return a[0] === b[0] && a[1] === b[1] && a[2] === b[2]
|
|
|
-}
|
|
|
-
|
|
|
-export class GoUpsellArtPainter {
|
|
|
- private panelRgb: Rgb = [0, 0, 0]
|
|
|
- private primaryRgb: Rgb = [255, 255, 255]
|
|
|
- private logoBaseRgb: Rgb = [180, 180, 180]
|
|
|
- private elapsed = 0
|
|
|
- private distances = new Float32Array(0)
|
|
|
- private edgeFalloff = new Float32Array(0)
|
|
|
- private geometryWidth = 0
|
|
|
- private geometryHeight = 0
|
|
|
- private reach = 1
|
|
|
- private logoX = 0
|
|
|
- private logoY = 0
|
|
|
- private logoIndexes = new Int32Array(0)
|
|
|
- private logoRgb: boolean | undefined
|
|
|
- private pulsePeak = 0
|
|
|
- private pulsePrimary = 0
|
|
|
- private cacheDirty = true
|
|
|
- private frameCache: Array<{ fg: Uint16Array; bg: Uint16Array }> = []
|
|
|
- private cacheBuildIndex = 0
|
|
|
-
|
|
|
- setBackgroundPanel(value: RGBA | Rgb | undefined) {
|
|
|
- if (!value) return false
|
|
|
- const next = value instanceof RGBA ? toRgb(value) : value
|
|
|
- if (sameRgb(this.panelRgb, next)) return false
|
|
|
- this.panelRgb = next
|
|
|
- this.invalidateCache()
|
|
|
- return true
|
|
|
- }
|
|
|
-
|
|
|
- setLogoBase(value: RGBA | Rgb | undefined) {
|
|
|
- if (!value) return false
|
|
|
- const next = value instanceof RGBA ? toRgb(value) : value
|
|
|
- if (sameRgb(this.logoBaseRgb, next)) return false
|
|
|
- this.logoBaseRgb = next
|
|
|
- this.invalidateCache()
|
|
|
- return true
|
|
|
- }
|
|
|
-
|
|
|
- setPrimary(value: RGBA | Rgb | undefined) {
|
|
|
- if (!value) return false
|
|
|
- const next = value instanceof RGBA ? toRgb(value) : value
|
|
|
- if (sameRgb(this.primaryRgb, next)) return false
|
|
|
- this.primaryRgb = next
|
|
|
- this.invalidateCache()
|
|
|
- return true
|
|
|
- }
|
|
|
-
|
|
|
- render(frameBuffer: OptimizedBuffer, options: GoUpsellArtRenderOptions = {}) {
|
|
|
- const rgb = options.rgb === true
|
|
|
- this.elapsed = (this.elapsed + (options.deltaTime ?? 0)) % PERIOD
|
|
|
- this.rebuildGeometry(frameBuffer, rgb)
|
|
|
- if (options.cache !== false) {
|
|
|
- this.drawCached(frameBuffer, rgb)
|
|
|
- return
|
|
|
- }
|
|
|
- this.drawBackground(frameBuffer, this.elapsed)
|
|
|
- this.drawLogo(frameBuffer, this.elapsed, rgb)
|
|
|
- }
|
|
|
-
|
|
|
- private invalidateCache() {
|
|
|
- this.cacheDirty = true
|
|
|
- this.cacheBuildIndex = 0
|
|
|
- this.frameCache = []
|
|
|
- }
|
|
|
-
|
|
|
- private rebuildGeometry(frameBuffer: OptimizedBuffer, rgb: boolean) {
|
|
|
- const width = frameBuffer.width
|
|
|
- const height = frameBuffer.height
|
|
|
- const geometryChanged = width !== this.geometryWidth || height !== this.geometryHeight
|
|
|
- const logoTemplateChanged = this.logoRgb !== rgb
|
|
|
- if (!geometryChanged && !logoTemplateChanged) return
|
|
|
-
|
|
|
- if (geometryChanged) {
|
|
|
- this.geometryWidth = width
|
|
|
- this.geometryHeight = height
|
|
|
- this.logoX = Math.max(0, Math.floor((width - LOGO_WIDTH) / 2))
|
|
|
- this.logoY = Math.max(
|
|
|
- 0,
|
|
|
- Math.min(Math.max(0, height - LOGO_HEIGHT), Math.round((height - LOGO_HEIGHT) / 2) + LOGO_TOP_BIAS),
|
|
|
- )
|
|
|
-
|
|
|
- const centerX = this.logoX + LOGO_WIDTH / 2
|
|
|
- const centerY = this.logoY + LOGO_HEIGHT / 2
|
|
|
- this.reach = Math.hypot(Math.max(centerX, width - centerX), Math.max(centerY, height - centerY) * 2) + TAIL
|
|
|
- this.distances = new Float32Array(width * height)
|
|
|
- this.edgeFalloff = new Float32Array(width * height)
|
|
|
-
|
|
|
- for (let y = 0; y < height; y++) {
|
|
|
- for (let x = 0; x < width; x++) {
|
|
|
- const index = y * width + x
|
|
|
- const dist = Math.hypot(x + 0.5 - centerX, (y + 0.5 - centerY) * 2)
|
|
|
- this.distances[index] = dist
|
|
|
- this.edgeFalloff[index] = Math.max(0, 1 - (dist / (this.reach * 0.85)) ** 2)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- this.logoRgb = rgb
|
|
|
- this.invalidateCache()
|
|
|
- this.rebuildCellTemplate(frameBuffer, rgb)
|
|
|
- }
|
|
|
-
|
|
|
- private drawCached(frameBuffer: OptimizedBuffer, rgb: boolean) {
|
|
|
- if (this.cacheDirty) this.startFrameCache(frameBuffer, rgb)
|
|
|
- if (this.cacheBuildIndex < CACHE_FRAME_COUNT) {
|
|
|
- this.buildFrameCache(frameBuffer, rgb)
|
|
|
- this.drawBackground(frameBuffer, this.elapsed)
|
|
|
- this.drawLogo(frameBuffer, this.elapsed, rgb)
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const frame = this.frameCache[Math.floor((this.elapsed / PERIOD) * CACHE_FRAME_COUNT) % CACHE_FRAME_COUNT]
|
|
|
- if (frame) {
|
|
|
- frameBuffer.buffers.fg.set(frame.fg)
|
|
|
- frameBuffer.buffers.bg.set(frame.bg)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private startFrameCache(frameBuffer: OptimizedBuffer, rgb: boolean) {
|
|
|
- this.frameCache = []
|
|
|
- this.cacheBuildIndex = 0
|
|
|
- this.rebuildCellTemplate(frameBuffer, rgb)
|
|
|
- this.cacheDirty = false
|
|
|
- }
|
|
|
-
|
|
|
- private buildFrameCache(frameBuffer: OptimizedBuffer, rgb: boolean) {
|
|
|
- const end = Math.min(CACHE_FRAME_COUNT, this.cacheBuildIndex + CACHE_FRAMES_PER_RENDER)
|
|
|
- for (; this.cacheBuildIndex < end; this.cacheBuildIndex++) {
|
|
|
- const t = (this.cacheBuildIndex / CACHE_FRAME_COUNT) * PERIOD
|
|
|
- this.drawBackground(frameBuffer, t)
|
|
|
- this.drawLogo(frameBuffer, t, rgb)
|
|
|
- this.frameCache.push({
|
|
|
- fg: new Uint16Array(frameBuffer.buffers.fg),
|
|
|
- bg: new Uint16Array(frameBuffer.buffers.bg),
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private rebuildCellTemplate(frameBuffer: OptimizedBuffer, rgb: boolean) {
|
|
|
- const buffers = frameBuffer.buffers
|
|
|
- buffers.char.fill(SPACE)
|
|
|
- buffers.attributes.fill(0)
|
|
|
-
|
|
|
- if (this.geometryWidth < LOGO_WIDTH || this.geometryHeight < LOGO_HEIGHT) {
|
|
|
- this.logoIndexes = new Int32Array(0)
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- this.logoIndexes = new Int32Array(LOGO_TEMPLATE.length)
|
|
|
- for (let i = 0; i < LOGO_TEMPLATE.length; i++) {
|
|
|
- const cell = LOGO_TEMPLATE[i]!
|
|
|
- const index = (this.logoY + cell.y) * this.geometryWidth + this.logoX + cell.x
|
|
|
- this.logoIndexes[i] = index
|
|
|
- buffers.attributes[index] = cell.attributes
|
|
|
- buffers.char[index] =
|
|
|
- cell.kind === LogoCellKind.Background
|
|
|
- ? SPACE
|
|
|
- : cell.kind === LogoCellKind.Top || cell.kind === LogoCellKind.ShadowTop
|
|
|
- ? TOP_HALF
|
|
|
- : cell.kind === LogoCellKind.Solid
|
|
|
- ? rgb
|
|
|
- ? TOP_HALF
|
|
|
- : FULL_BLOCK
|
|
|
- : cell.charCode
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private drawBackground(frameBuffer: OptimizedBuffer, t: number) {
|
|
|
- const buffers = frameBuffer.buffers
|
|
|
- const fg = buffers.fg
|
|
|
- const bg = buffers.bg
|
|
|
- const distances = this.distances
|
|
|
- const edgeFalloff = this.edgeFalloff
|
|
|
- const baseR = this.panelRgb[0]
|
|
|
- const baseG = this.panelRgb[1]
|
|
|
- const baseB = this.panelRgb[2]
|
|
|
- const deltaR = this.primaryRgb[0] - baseR
|
|
|
- const deltaG = this.primaryRgb[1] - baseG
|
|
|
- const deltaB = this.primaryRgb[2] - baseB
|
|
|
- const breath = (0.5 + 0.5 * Math.sin(t * BREATH_SPEED)) * BREATH_AMP
|
|
|
-
|
|
|
- const phase0 = (t / PERIOD - PHASE_OFFSET + 1) % 1
|
|
|
- const phase1 = (t / PERIOD + 1 / RINGS - PHASE_OFFSET + 1) % 1
|
|
|
- const phase2 = (t / PERIOD + 2 / RINGS - PHASE_OFFSET + 1) % 1
|
|
|
- const envelope0 = Math.sin(phase0 * Math.PI)
|
|
|
- const envelope1 = Math.sin(phase1 * Math.PI)
|
|
|
- const envelope2 = Math.sin(phase2 * Math.PI)
|
|
|
- const eased0 = envelope0 * envelope0 * (3 - 2 * envelope0)
|
|
|
- const eased1 = envelope1 * envelope1 * (3 - 2 * envelope1)
|
|
|
- const eased2 = envelope2 * envelope2 * (3 - 2 * envelope2)
|
|
|
- const head0 = phase0 * this.reach
|
|
|
- const head1 = phase1 * this.reach
|
|
|
- const head2 = phase2 * this.reach
|
|
|
-
|
|
|
- for (let index = 0; index < distances.length; index++) {
|
|
|
- const dist = distances[index]
|
|
|
- const delta0 = dist - head0
|
|
|
- const abs0 = delta0 < 0 ? -delta0 : delta0
|
|
|
- const crest0 = abs0 < WIDTH ? 0.5 + 0.5 * Math.cos((delta0 / WIDTH) * Math.PI) : 0
|
|
|
- const tail0 = delta0 < 0 && delta0 > -TAIL ? (1 + delta0 * TAIL_SCALE) ** 2.3 : 0
|
|
|
-
|
|
|
- const delta1 = dist - head1
|
|
|
- const abs1 = delta1 < 0 ? -delta1 : delta1
|
|
|
- const crest1 = abs1 < WIDTH ? 0.5 + 0.5 * Math.cos((delta1 / WIDTH) * Math.PI) : 0
|
|
|
- const tail1 = delta1 < 0 && delta1 > -TAIL ? (1 + delta1 * TAIL_SCALE) ** 2.3 : 0
|
|
|
-
|
|
|
- const delta2 = dist - head2
|
|
|
- const abs2 = delta2 < 0 ? -delta2 : delta2
|
|
|
- const crest2 = abs2 < WIDTH ? 0.5 + 0.5 * Math.cos((delta2 / WIDTH) * Math.PI) : 0
|
|
|
- const tail2 = delta2 < 0 && delta2 > -TAIL ? (1 + delta2 * TAIL_SCALE) ** 2.3 : 0
|
|
|
-
|
|
|
- const level =
|
|
|
- (crest0 * AMP + tail0 * TAIL_AMP) * eased0 +
|
|
|
- (crest1 * AMP + tail1 * TAIL_AMP) * eased1 +
|
|
|
- (crest2 * AMP + tail2 * TAIL_AMP) * eased2
|
|
|
- const rawStrength = (level * RING_SCALE + breath) * edgeFalloff[index]
|
|
|
- const strength = (rawStrength > 1 ? 1 : rawStrength) * 0.7
|
|
|
- const offset = index * 4
|
|
|
- const r = Math.round(baseR + deltaR * strength)
|
|
|
- const g = Math.round(baseG + deltaG * strength)
|
|
|
- const b = Math.round(baseB + deltaB * strength)
|
|
|
- bg[offset] = fg[offset] = r
|
|
|
- bg[offset + 1] = fg[offset + 1] = g
|
|
|
- bg[offset + 2] = fg[offset + 2] = b
|
|
|
- bg[offset + 3] = fg[offset + 3] = 255
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private setLogoPulse(dist: number, head0: number, eased0: number, head1: number, eased1: number) {
|
|
|
- let peak = 0.04
|
|
|
- let primary = 0
|
|
|
-
|
|
|
- const delta0 = dist - head0
|
|
|
- const core0 = Math.exp(-(Math.abs(delta0 / 1.2) ** 1.8))
|
|
|
- const soft0 = Math.exp(-(Math.abs(delta0 / 7) ** 1.6))
|
|
|
- const tail0 = delta0 < 0 && delta0 > -7 ? (1 + delta0 / 7) ** 2.6 : 0
|
|
|
- peak += core0 * 0.65 * eased0
|
|
|
- primary += (soft0 * 0.16 + tail0 * 0.22) * eased0
|
|
|
-
|
|
|
- const delta1 = dist - head1
|
|
|
- const core1 = Math.exp(-(Math.abs(delta1 / 1.2) ** 1.8))
|
|
|
- const soft1 = Math.exp(-(Math.abs(delta1 / 7) ** 1.6))
|
|
|
- const tail1 = delta1 < 0 && delta1 > -7 ? (1 + delta1 / 7) ** 2.6 : 0
|
|
|
- peak += core1 * 0.65 * eased1
|
|
|
- primary += (soft1 * 0.16 + tail1 * 0.22) * eased1
|
|
|
-
|
|
|
- this.pulsePeak = peak > 1 ? 1 : peak
|
|
|
- this.pulsePrimary = primary > 1 ? 1 : primary
|
|
|
- }
|
|
|
-
|
|
|
- private drawLogo(frameBuffer: OptimizedBuffer, t: number, rgb: boolean) {
|
|
|
- if (this.logoIndexes.length === 0) return
|
|
|
-
|
|
|
- const buffers = frameBuffer.buffers
|
|
|
- const fg = buffers.fg
|
|
|
- const bg = buffers.bg
|
|
|
- const shadow: Rgb = [
|
|
|
- mixChannel(this.panelRgb[0], this.logoBaseRgb[0], 0.25),
|
|
|
- mixChannel(this.panelRgb[1], this.logoBaseRgb[1], 0.25),
|
|
|
- mixChannel(this.panelRgb[2], this.logoBaseRgb[2], 0.25),
|
|
|
- ]
|
|
|
- const phase0 = (t / PERIOD) % 1
|
|
|
- const phase1 = (t / PERIOD + 0.5) % 1
|
|
|
- const envelope0 = Math.sin(phase0 * Math.PI)
|
|
|
- const envelope1 = Math.sin(phase1 * Math.PI)
|
|
|
- const eased0 = envelope0 * envelope0 * (3 - 2 * envelope0)
|
|
|
- const eased1 = envelope1 * envelope1 * (3 - 2 * envelope1)
|
|
|
- const head0 = phase0 * LOGO_REACH
|
|
|
- const head1 = phase1 * LOGO_REACH
|
|
|
-
|
|
|
- for (let i = 0; i < LOGO_TEMPLATE.length; i++) {
|
|
|
- const cell = LOGO_TEMPLATE[i]!
|
|
|
- const index = this.logoIndexes[i]!
|
|
|
- const offset = index * 4
|
|
|
- this.setLogoPulse(cell.topDist, head0, eased0, head1, eased1)
|
|
|
- const topPeak = this.pulsePeak
|
|
|
- const topPrimary = this.pulsePrimary
|
|
|
- this.setLogoPulse(cell.bottomDist, head0, eased0, head1, eased1)
|
|
|
- const bottomPeak = this.pulsePeak
|
|
|
- const bottomPrimary = this.pulsePrimary
|
|
|
-
|
|
|
- if (cell.kind === LogoCellKind.Background) {
|
|
|
- writeLogoTint(bg, offset, shadow, this.primaryRgb, 0, Math.max(topPeak, bottomPeak) * 0.18)
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- if (cell.kind === LogoCellKind.Top) {
|
|
|
- writeLogoTint(fg, offset, this.logoBaseRgb, this.primaryRgb, topPrimary, topPeak)
|
|
|
- writeLogoTint(bg, offset, shadow, this.primaryRgb, 0, bottomPeak * 0.18)
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- if (cell.kind === LogoCellKind.ShadowTop) {
|
|
|
- writeLogoTint(fg, offset, shadow, this.primaryRgb, 0, topPeak * 0.18)
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- if (cell.kind === LogoCellKind.Solid && rgb) {
|
|
|
- writeLogoTint(fg, offset, this.logoBaseRgb, this.primaryRgb, topPrimary, topPeak)
|
|
|
- writeLogoTint(bg, offset, this.logoBaseRgb, this.primaryRgb, bottomPrimary, bottomPeak)
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- writeLogoTint(
|
|
|
- fg,
|
|
|
- offset,
|
|
|
- this.logoBaseRgb,
|
|
|
- this.primaryRgb,
|
|
|
- (topPrimary + bottomPrimary) / 2,
|
|
|
- (topPeak + bottomPeak) / 2,
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
-}
|